diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-20 11:37:31 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-20 11:37:31 +0000 |
| commit | aa86729f9a2ab95346a2851e3837de1c367aae17 (patch) | |
| tree | b601b18b6724f2fb449c7fa9ea50cbd652a8077d | |
| parent | 95bbe9c583ff841220da1267630e7b2025fc36dc (diff) | |
(대표님) 20250620 작업사항
54 files changed, 26734 insertions, 818 deletions
diff --git a/app/[lng]/evcp/(evcp)/evaluation/page.tsx b/app/[lng]/evcp/(evcp)/evaluation/page.tsx new file mode 100644 index 00000000..3ae3272a --- /dev/null +++ b/app/[lng]/evcp/(evcp)/evaluation/page.tsx @@ -0,0 +1,194 @@ +// ================================================================ +// 4. PERIODIC EVALUATIONS PAGE +// ================================================================ + +import * as React from "react" +import { Metadata } from "next" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { HelpCircle } from "lucide-react" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table" + +export const metadata: Metadata = { + title: "협력업체 정기평가", + description: "협력업체 정기평가 진행 현황을 관리합니다.", +} + +interface PeriodicEvaluationsPageProps { + searchParams: Promise<SearchParams> +} + +// 프로세스 안내 팝오버 컴포넌트 +function ProcessGuidePopover() { + return ( + <Popover> + <PopoverTrigger asChild> + <Button variant="ghost" size="icon" className="h-6 w-6"> + <HelpCircle className="h-4 w-4 text-muted-foreground" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-96" align="start"> + <div className="space-y-3"> + <div className="space-y-1"> + <h4 className="font-medium">정기평가 프로세스</h4> + <p className="text-sm text-muted-foreground"> + 확정된 평가 대상 업체들에 대한 정기평가 절차입니다. + </p> + </div> + <div className="space-y-3 text-sm"> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 1 + </div> + <div> + <p className="font-medium">평가 대상 확정</p> + <p className="text-muted-foreground">평가 대상으로 확정된 업체들의 정기평가가 자동 생성됩니다.</p> + </div> + </div> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 2 + </div> + <div> + <p className="font-medium">업체 자료 제출</p> + <p className="text-muted-foreground">각 업체는 평가에 필요한 자료를 제출 마감일까지 제출해야 합니다.</p> + </div> + </div> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 3 + </div> + <div> + <p className="font-medium">평가자 검토</p> + <p className="text-muted-foreground">지정된 평가자들이 평가표를 기반으로 점수를 매기고 검토합니다.</p> + </div> + </div> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 4 + </div> + <div> + <p className="font-medium">최종 확정</p> + <p className="text-muted-foreground">모든 평가가 완료되면 최종 점수와 등급이 확정됩니다.</p> + </div> + </div> + </div> + </div> + </PopoverContent> + </Popover> + ) +} + +// TODO: 이 함수들은 실제 서비스 파일에서 구현해야 함 +function getDefaultEvaluationYear() { + return new Date().getFullYear() +} + +function searchParamsPeriodicEvaluationsCache() { + // TODO: 실제 파서 구현 + return { + parse: (params: any) => params + } +} + +async function getPeriodicEvaluations(params: any) { + // TODO: 실제 API 호출 구현 + return { + data: [], + total: 0, + pageCount: 0 + } +} + +export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) { + const searchParams = await props.searchParams + const search = searchParamsPeriodicEvaluationsCache().parse(searchParams) + const validFilters = getValidFilters(search.filters || []) + + // 기본 필터 처리 + let basicFilters = [] + if (search.basicFilters && search.basicFilters.length > 0) { + basicFilters = search.basicFilters + } + + // 모든 필터를 합쳐서 처리 + const allFilters = [...validFilters, ...basicFilters] + + // 조인 연산자 + const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; + + // 현재 평가년도 + const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() + + // Promise.all로 감싸서 전달 + const promises = Promise.all([ + getPeriodicEvaluations({ + ...search, + filters: allFilters, + joinOperator, + }) + ]) + + return ( + <Shell className="gap-4"> + {/* 헤더 */} + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center gap-2"> + <h2 className="text-2xl font-bold tracking-tight"> + 협력업체 정기평가 + </h2> + <Badge variant="outline" className="text-sm"> + {currentEvaluationYear}년도 + </Badge> + <ProcessGuidePopover /> + </div> + </div> + </div> + + {/* 메인 테이블 */} + <React.Suspense + key={JSON.stringify(searchParams)} + fallback={ + <DataTableSkeleton + columnCount={15} + searchableColumnCount={2} + filterableColumnCount={8} + cellWidths={[ + "3rem", // checkbox + "5rem", // 평가년도 + "5rem", // 평가기간 + "4rem", // 구분 + "8rem", // 벤더코드 + "12rem", // 벤더명 + "4rem", // 내외자 + "6rem", // 자재구분 + "5rem", // 문서제출 + "4rem", // 제출일 + "4rem", // 마감일 + "4rem", // 총점 + "4rem", // 등급 + "5rem", // 진행상태 + "8rem" // actions + ]} + shrinkZero + /> + } + > + <PeriodicEvaluationsTable + promises={promises} + evaluationYear={currentEvaluationYear} + /> + </React.Suspense> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/project-gtc/page.tsx b/app/[lng]/evcp/(evcp)/project-gtc/page.tsx new file mode 100644 index 00000000..8e12a489 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/project-gtc/page.tsx @@ -0,0 +1,63 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getProjectGtcList } from "@/lib/project-gtc/service" +import { projectGtcSearchParamsSchema } from "@/lib/project-gtc/validations" +import { ProjectGtcTable } from "@/lib/project-gtc/table/project-gtc-table" + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = projectGtcSearchParamsSchema.parse(searchParams) + + const promises = Promise.all([ + getProjectGtcList({ + page: search.page, + perPage: search.perPage, + search: search.search, + sort: search.sort, + }), + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Project GTC + </h2> + <p className="text-muted-foreground"> + 프로젝트별 GTC(General Terms and Conditions) 파일을 관리할 수 있습니다. + 각 프로젝트마다 하나의 GTC 파일을 업로드할 수 있으며, 파일 업로드 시 기존 파일은 자동으로 교체됩니다. + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* 추가 기능이 필요하면 여기에 추가 */} + </React.Suspense> + + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={8} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["3rem", "3rem", "12rem", "20rem", "10rem", "20rem", "15rem", "12rem", "3rem"]} + shrinkZero + /> + } + > + <ProjectGtcTable promises={promises} /> + </React.Suspense> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/api/project-gtc/route.ts b/app/api/project-gtc/route.ts new file mode 100644 index 00000000..8fe4ad2e --- /dev/null +++ b/app/api/project-gtc/route.ts @@ -0,0 +1,154 @@ +import { NextRequest, NextResponse } from "next/server" +import { uploadProjectGtcFile, deleteProjectGtcFile, getProjectGtcList, getProjectGtcFile } from "@/lib/project-gtc/service" +import { promises as fs } from "fs" +import path from "path" + +// 파일 다운로드 +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const projectId = searchParams.get("projectId") + const action = searchParams.get("action") + + // 목록 조회 + if (action !== "download") { + const page = parseInt(searchParams.get("page") || "1") + const perPage = parseInt(searchParams.get("perPage") || "10") + const search = searchParams.get("search") || "" + const sortParam = searchParams.get("sort") + + let sort: Array<{ id: string; desc: boolean }> = [] + if (sortParam) { + try { + sort = JSON.parse(sortParam) + } catch { + sort = [{ id: "projectCreatedAt", desc: true }] + } + } else { + sort = [{ id: "projectCreatedAt", desc: true }] + } + + const result = await getProjectGtcList({ + page, + perPage, + search, + sort, + }) + + return NextResponse.json(result) + } + + // 파일 다운로드 + if (!projectId) { + return NextResponse.json( + { error: "프로젝트 ID가 필요합니다." }, + { status: 400 } + ) + } + + const fileInfo = await getProjectGtcFile(parseInt(projectId)) + + if (!fileInfo) { + return NextResponse.json( + { error: "파일을 찾을 수 없습니다." }, + { status: 404 } + ) + } + + // 파일 경로 구성 + const filePath = path.join(process.cwd(), "public", fileInfo.filePath) + + try { + // 파일 읽기 + const fileBuffer = await fs.readFile(filePath) + + // 응답 헤더 설정 + const headers = new Headers() + headers.set('Content-Type', fileInfo.mimeType || 'application/octet-stream') + headers.set('Content-Disposition', `attachment; filename="${encodeURIComponent(fileInfo.originalFileName)}"`) + headers.set('Content-Length', fileInfo.fileSize?.toString() || '0') + + return new NextResponse(new Uint8Array(fileBuffer), { + status: 200, + headers, + }) + } catch (fileError) { + console.error("파일 읽기 오류:", fileError) + return NextResponse.json( + { error: "파일을 읽을 수 없습니다." }, + { status: 500 } + ) + } + } catch (error) { + console.error("Project GTC API 에러:", error) + return NextResponse.json( + { error: "요청 처리 중 오류가 발생했습니다." }, + { status: 500 } + ) + } +} + +// 파일 업로드 +export async function POST(request: NextRequest) { + try { + const formData = await request.formData() + const projectId = formData.get("projectId") as string + const file = formData.get("file") as File + + if (!projectId || !file) { + return NextResponse.json( + { error: "프로젝트 ID와 파일이 필요합니다." }, + { status: 400 } + ) + } + + const result = await uploadProjectGtcFile(parseInt(projectId), file) + + if (result.success) { + return NextResponse.json({ success: true, data: result.data }) + } else { + return NextResponse.json( + { error: result.error }, + { status: 400 } + ) + } + } catch (error) { + console.error("Project GTC 파일 업로드 API 에러:", error) + return NextResponse.json( + { error: "파일 업로드 중 오류가 발생했습니다." }, + { status: 500 } + ) + } +} + +// 파일 삭제 +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const projectId = searchParams.get("projectId") + + if (!projectId) { + return NextResponse.json( + { error: "프로젝트 ID가 필요합니다." }, + { status: 400 } + ) + } + + const result = await deleteProjectGtcFile(parseInt(projectId)) + + if (result.success) { + return NextResponse.json({ success: true }) + } else { + return NextResponse.json( + { error: result.error }, + { status: 400 } + ) + } + } catch (error) { + console.error("Project GTC 파일 삭제 API 에러:", error) + return NextResponse.json( + { error: "파일 삭제 중 오류가 발생했습니다." }, + { status: 500 } + ) + } +}
\ No newline at end of file diff --git a/components/data-table/use-table-presets.tsx b/components/data-table/use-table-presets.tsx index 5e641762..f9ecda2a 100644 --- a/components/data-table/use-table-presets.tsx +++ b/components/data-table/use-table-presets.tsx @@ -288,17 +288,19 @@ export function useTablePresets<TData>( }, [mutate]) // 클라이언트 상태 업데이트 (컬럼 가시성, 핀 등) - const updateClientState = useCallback(async (newClientState: Partial<TableSettings<TData>>) => { - if (!activePreset) return - - const updatedSettings = { - ...activePreset.settings, - ...newClientState - } - - await updatePreset(activePreset.id, updatedSettings) - }, [activePreset, updatePreset]) + const updateClientState = useCallback( + async (newClientState: Partial<TableSettings<TData>>) => { + if (!activePreset) return; + + + const prev = activePreset.settings; + const next = { ...prev, ...newClientState }; + + await updatePreset(activePreset.id, next); + }, + [activePreset, updatePreset, tableId] // ← tableId 도 의존성에 넣어 둡니다 + ); // URL 변경 감지 및 미저장 변경사항 체크 useEffect(() => { if (!isClient || !presets || !activePreset) return diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx index 61e9897f..b59684e3 100644 --- a/components/form-data/form-data-table.tsx +++ b/components/form-data/form-data-table.tsx @@ -57,6 +57,7 @@ import { SEDPConfirmationDialog, SEDPStatusDialog } from "./sedp-components"; import { SEDPCompareDialog } from "./sedp-compare-dialog"; import { getSEDPToken } from "@/lib/sedp/sedp-token"; import { DeleteFormDataDialog } from "./delete-form-data-dialog"; +import { TemplateViewDialog } from "./spreadJS-dialog"; // 기존 fetchTagDataFromSEDP 함수 async function fetchTagDataFromSEDP(projectCode: string, formCode: string): Promise<any> { @@ -328,12 +329,12 @@ export default function DynamicTable({ try { setIsLoadingTemplate(true); - + const templateResult = await fetchTemplateFromSEDP(projectCode, formCode); - + setTemplateData(templateResult); setTemplateDialogOpen(true); - + toast.success("Template data loaded successfully"); } catch (error) { console.error("Error fetching template:", error); @@ -720,21 +721,6 @@ export default function DynamicTable({ </Button> )} - {/* 새로 추가된 Template 보기 버튼 */} - <Button - variant="outline" - size="sm" - onClick={handleGetTemplate} - disabled={isAnyOperationPending || selectedRowCount !== 1} - > - {isLoadingTemplate ? ( - <Loader className="mr-2 size-4 animate-spin" /> - ) : ( - <Eye className="mr-2 size-4" /> - )} - View In Spread - </Button> - {/* 버튼 그룹 */} <div className="flex items-center gap-2"> {/* 태그 관리 드롭다운 */} @@ -827,7 +813,25 @@ export default function DynamicTable({ Export </Button> - + {/* 새로 추가된 Template 보기 버튼 */} + <Button + variant="outline" + size="sm" + onClick={handleGetTemplate} + disabled={isAnyOperationPending || selectedRowCount !== 1} + > + {isLoadingTemplate ? ( + <Loader className="mr-2 size-4 animate-spin" /> + ) : ( + <Eye className="mr-2 size-4" /> + )} + View Template + {selectedRowCount === 1 && ( + <span className="ml-2 text-xs bg-green-100 text-green-700 px-2 py-1 rounded"> + 1 + </span> + )} + </Button> {/* COMPARE WITH SEDP 버튼 */} <Button @@ -918,6 +922,18 @@ export default function DynamicTable({ templateData={templateData} selectedRow={selectedRowsData[0]} formCode={formCode} + contractItemId={contractItemId} + onUpdateSuccess={(updatedValues) => { + // SpreadSheets에서 업데이트된 값을 테이블에 반영 + const tagNo = updatedValues.TAG_NO; + if (tagNo) { + setTableData(prev => + prev.map(item => + item.TAG_NO === tagNo ? updatedValues : item + ) + ); + } + }} /> {/* SEDP Confirmation Dialog */} @@ -989,5 +1005,4 @@ export default function DynamicTable({ )} </> ); -} - +}
\ No newline at end of file diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx index 69232508..4a8550cb 100644 --- a/components/form-data/spreadJS-dialog.tsx +++ b/components/form-data/spreadJS-dialog.tsx @@ -1,69 +1,340 @@ +"use client"; + import * as React from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { GenericData } from "./export-excel-form"; import { SpreadSheets, Worksheet, Column } from "@mescius/spread-sheets-react"; import * as GC from "@mescius/spread-sheets"; +import { toast } from "sonner"; +import { updateFormDataInDB } from "@/lib/forms/services"; +import { Loader, Save } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +interface TemplateItem { + TMPL_ID: string; + NAME: string; + TMPL_TYPE: string; + SPR_LST_SETUP: { + ACT_SHEET: string; + HIDN_SHEETS: Array<string>; + CONTENT?: string; // SpreadSheets JSON + DATA_SHEETS: Array<{ + SHEET_NAME: string; + REG_TYPE_ID: string; + MAP_CELL_ATT: Array<{ + ATT_ID: string; + IN: string; + }>; + }>; + }; + GRD_LST_SETUP: { + REG_TYPE_ID: string; + SPR_ITM_IDS: Array<string>; + ATTS: Array<{}>; + }; + SPR_ITM_LST_SETUP: { + ACT_SHEET: string; + HIDN_SHEETS: Array<string>; + CONTENT?: string; // SpreadSheets JSON + DATA_SHEETS: Array<{ + SHEET_NAME: string; + REG_TYPE_ID: string; + MAP_CELL_ATT: Array<{ + ATT_ID: string; + IN: string; + }>; + }>; + }; +} interface TemplateViewDialogProps { - isOpen: boolean; - onClose: () => void; - templateData: any; - selectedRow: GenericData; - formCode: string; - } + isOpen: boolean; + onClose: () => void; + templateData: TemplateItem[] | any; // 배열 또는 기존 형태 + selectedRow: GenericData; + formCode: string; + contractItemId: number; + /** 업데이트 성공 시 호출될 콜백 */ + onUpdateSuccess?: (updatedValues: Record<string, any>) => void; +} export function TemplateViewDialog({ - isOpen, - onClose, - templateData, - selectedRow, - formCode - }: TemplateViewDialogProps) { - return ( - <Dialog open={isOpen} onOpenChange={onClose}> - <DialogContent className="w-[80%] max-w-none h-[80vh] flex flex-col" style={{maxWidth:"80vw"}}> - <DialogHeader className="flex-shrink-0"> - <DialogTitle>SEDP Template - {formCode}</DialogTitle> - <DialogDescription> - {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || 'N/A'}`} - </DialogDescription> - </DialogHeader> - - {/* 스크롤 가능한 콘텐츠 영역 */} - <div className="flex-1 overflow-y-auto space-y-4 py-4"> - {/* 여기에 템플릿 데이터나 다른 콘텐츠가 들어갈 예정 */} - <div className="space-y-4"> - <p className="text-sm text-muted-foreground"> - Template content will be displayed here... - </p> - - {/* 임시로 templateData 표시 */} - {templateData && ( - <pre className="bg-muted p-4 rounded text-sm overflow-auto"> - {JSON.stringify(templateData, null, 2)} - </pre> - )} + isOpen, + onClose, + templateData, + selectedRow, + formCode, + contractItemId, + onUpdateSuccess +}: TemplateViewDialogProps) { + const [hostStyle, setHostStyle] = React.useState({ + width: '100%', + height: '100%' + }); + + const [isPending, setIsPending] = React.useState(false); + const [hasChanges, setHasChanges] = React.useState(false); + const [currentSpread, setCurrentSpread] = React.useState<any>(null); + const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>(""); + + // 템플릿 데이터를 배열로 정규화하고 CONTENT가 있는 것만 필터링 + const normalizedTemplates = React.useMemo((): TemplateItem[] => { + if (!templateData) return []; + + let templates: TemplateItem[]; + // 이미 배열인 경우 + if (Array.isArray(templateData)) { + templates = templateData as TemplateItem[]; + } else { + // 기존 형태인 경우 (하위 호환성) + templates = [templateData as TemplateItem]; + } + + // CONTENT가 있는 템플릿만 필터링 + return templates.filter(template => { + const sprContent = template.SPR_LST_SETUP?.CONTENT; + const sprItmContent = template.SPR_ITM_LST_SETUP?.CONTENT; + return sprContent || sprItmContent; + }); + }, [templateData]); + + // 선택된 템플릿 가져오기 + const selectedTemplate = React.useMemo(() => { + if (!selectedTemplateId) return normalizedTemplates[0]; // 기본값: 첫 번째 템플릿 + return normalizedTemplates.find(t => t.TMPL_ID === selectedTemplateId) || normalizedTemplates[0]; + }, [normalizedTemplates, selectedTemplateId]); + + // 템플릿 변경 시 기본 선택 + React.useEffect(() => { + if (normalizedTemplates.length > 0 && !selectedTemplateId) { + setSelectedTemplateId(normalizedTemplates[0].TMPL_ID); + } + }, [normalizedTemplates, selectedTemplateId]); + + const initSpread = React.useCallback((spread: any) => { + if (!spread || !selectedTemplate) return; + + try { + setCurrentSpread(spread); + setHasChanges(false); // 템플릿 로드 시 변경사항 초기화 + + // CONTENT 찾기 (SPR_LST_SETUP 또는 SPR_ITM_LST_SETUP 중 하나) + let contentJson = null; + if (selectedTemplate.SPR_LST_SETUP?.CONTENT) { + contentJson = selectedTemplate.SPR_LST_SETUP.CONTENT; + console.log('Using SPR_LST_SETUP.CONTENT for template:', selectedTemplate.NAME); + } else if (selectedTemplate.SPR_ITM_LST_SETUP?.CONTENT) { + contentJson = selectedTemplate.SPR_ITM_LST_SETUP.CONTENT; + console.log('Using SPR_ITM_LST_SETUP.CONTENT for template:', selectedTemplate.NAME); + } + + if (contentJson) { + console.log('Loading template content for:', selectedTemplate.NAME); + + const jsonData = typeof contentJson === 'string' + ? JSON.parse(contentJson) + : contentJson; + + // fromJSON으로 템플릿 구조 로드 + spread.fromJSON(jsonData); + } else { + console.warn('No CONTENT found in template:', selectedTemplate.NAME); + return; + } + + // 값 변경 이벤트 리스너 추가 (간단한 변경사항 감지만) + const activeSheet = spread.getActiveSheet(); + + activeSheet.bind(GC.Spread.Sheets.Events.CellChanged, (event: any, info: any) => { + console.log('Cell changed:', info); + setHasChanges(true); + }); + + activeSheet.bind(GC.Spread.Sheets.Events.ValueChanged, (event: any, info: any) => { + console.log('Value changed:', info); + setHasChanges(true); + }); + + } catch (error) { + console.error('Error initializing spread:', error); + toast.error('Failed to load template'); + } + }, [selectedTemplate]); + + // 템플릿 변경 핸들러 + const handleTemplateChange = (templateId: string) => { + setSelectedTemplateId(templateId); + setHasChanges(false); // 템플릿 변경 시 변경사항 초기화 + + // SpreadSheets 재초기화는 useCallback 의존성에 의해 자동으로 처리됨 + if (currentSpread) { + // 강제로 재초기화 + setTimeout(() => { + initSpread(currentSpread); + }, 100); + } + }; + + // 변경사항 저장 함수 + const handleSaveChanges = React.useCallback(async () => { + if (!currentSpread || !hasChanges) { + toast.info("No changes to save"); + return; + } + + try { + setIsPending(true); + + // SpreadSheets에서 현재 데이터를 JSON으로 추출 + const spreadJson = currentSpread.toJSON(); + console.log('Current spread data:', spreadJson); + + // 실제 데이터 추출 방법은 SpreadSheets 구조에 따라 달라질 수 있음 + // 여기서는 기본적인 예시만 제공 + const activeSheet = currentSpread.getActiveSheet(); + + // 간단한 예시: 특정 범위의 데이터를 추출하여 selectedRow 형태로 변환 + // 실제 구현에서는 템플릿의 구조에 맞춰 데이터를 추출해야 함 + const dataToSave = { + ...selectedRow, // 기본값으로 원본 데이터 사용 + // 여기에 SpreadSheets에서 변경된 값들을 추가 + // 예: TAG_DESC: activeSheet.getValue(특정행, 특정열) + }; + + // TAG_NO는 절대 변경되지 않도록 원본 값으로 강제 설정 + dataToSave.TAG_NO = selectedRow?.TAG_NO; + + console.log('Data to save (TAG_NO preserved):', dataToSave); + + const { success, message } = await updateFormDataInDB( + formCode, + contractItemId, + dataToSave + ); + + if (!success) { + toast.error(message); + return; + } + + toast.success("Changes saved successfully!"); + + const updatedData = { + ...selectedRow, + ...dataToSave, + }; + + onUpdateSuccess?.(updatedData); + setHasChanges(false); + + } catch (error) { + console.error("Error saving changes:", error); + toast.error("An unexpected error occurred while saving"); + } finally { + setIsPending(false); + } + }, [currentSpread, hasChanges, formCode, contractItemId, selectedRow, onUpdateSuccess]); + + if (!isOpen) return null; + + return ( + <Dialog open={isOpen} onOpenChange={onClose}> + <DialogContent + className="w-[80%] max-w-none h-[80vh] flex flex-col" + style={{maxWidth:"80vw"}} + > + <DialogHeader className="flex-shrink-0"> + <DialogTitle>SEDP Template - {formCode}</DialogTitle> + <DialogDescription> + {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || 'N/A'}`} + {hasChanges && ( + <span className="ml-2 text-orange-600 font-medium"> + • Unsaved changes + </span> + )} + <br /> + <span className="text-xs text-muted-foreground"> + Template content will be loaded directly. Manual data entry may be required. + </span> + </DialogDescription> + </DialogHeader> + + {/* 템플릿 선택 UI */} + {normalizedTemplates.length > 1 && ( + <div className="flex-shrink-0 px-4 py-2 border-b"> + <div className="flex items-center gap-2"> + <label className="text-sm font-medium">Template:</label> + <Select value={selectedTemplateId} onValueChange={handleTemplateChange}> + <SelectTrigger className="w-64"> + <SelectValue placeholder="Select a template" /> + </SelectTrigger> + <SelectContent> + {normalizedTemplates.map((template) => ( + <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}> + <div className="flex flex-col"> + <span>{template.NAME || `Template ${template.TMPL_ID.slice(0, 8)}`}</span> + <span className="text-xs text-muted-foreground">{template.TMPL_TYPE}</span> + </div> + </SelectItem> + ))} + </SelectContent> + </Select> + <span className="text-xs text-muted-foreground"> + ({normalizedTemplates.length} templates available) + </span> </div> </div> - - <DialogFooter className="flex-shrink-0"> - <Button variant="outline" onClick={onClose}> - Close - </Button> + )} + + {/* SpreadSheets 컴포넌트 영역 */} + <div className="flex-1 overflow-hidden"> + {selectedTemplate ? ( + <SpreadSheets + key={selectedTemplateId} // 템플릿 변경 시 컴포넌트 재생성 + workbookInitialized={initSpread} + hostStyle={hostStyle} + /> + ) : ( + <div className="flex items-center justify-center h-full text-muted-foreground"> + No template available + </div> + )} + </div> + + <DialogFooter className="flex-shrink-0"> + <Button variant="outline" onClick={onClose}> + Close + </Button> + + {hasChanges && ( <Button variant="default" - onClick={() => { - // 여기에 Template 적용 로직 추가 가능 - console.log('Apply template logic here'); - onClose(); - }} + onClick={handleSaveChanges} + disabled={isPending} > - Apply Template + {isPending ? ( + <> + <Loader className="mr-2 h-4 w-4 animate-spin" /> + Saving... + </> + ) : ( + <> + <Save className="mr-2 h-4 w-4" /> + Save Changes + </> + )} </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ); - }
\ No newline at end of file + )} + + </DialogFooter> + </DialogContent> + </Dialog> + ); +}
\ No newline at end of file diff --git a/db/migrations/0157_oval_sunfire.sql b/db/migrations/0157_oval_sunfire.sql new file mode 100644 index 00000000..71cb063f --- /dev/null +++ b/db/migrations/0157_oval_sunfire.sql @@ -0,0 +1,14 @@ +CREATE TABLE "project_gtc_files" ( + "id" serial PRIMARY KEY NOT NULL, + "project_id" integer NOT NULL, + "file_name" varchar(255) NOT NULL, + "file_path" varchar(1024) NOT NULL, + "original_file_name" varchar(255) NOT NULL, + "file_size" integer, + "mime_type" varchar(100), + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "project_gtc_files" ADD CONSTRAINT "project_gtc_files_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE VIEW "public"."project_gtc_view" AS (select "projects"."id" as "id", "projects"."code" as "code", "projects"."name" as "name", "projects"."type" as "type", "projects"."created_at" as "project_created_at", "projects"."updated_at" as "project_updated_at", "project_gtc_files"."id" as "gtc_file_id", "project_gtc_files"."file_name" as "fileName", "project_gtc_files"."file_path" as "filePath", "project_gtc_files"."original_file_name" as "originalFileName", "project_gtc_files"."file_size" as "fileSize", "project_gtc_files"."mime_type" as "mimeType", "project_gtc_files"."created_at" as "gtcCreatedAt", "project_gtc_files"."updated_at" as "gtcUpdatedAt" from "projects" left join "project_gtc_files" on "projects"."id" = "project_gtc_files"."project_id");
\ No newline at end of file diff --git a/db/migrations/meta/0157_snapshot.json b/db/migrations/meta/0157_snapshot.json new file mode 100644 index 00000000..a8f09c62 --- /dev/null +++ b/db/migrations/meta/0157_snapshot.json @@ -0,0 +1,17486 @@ +{ + "id": "daa38495-b1be-415f-a1af-1be1268e9c30", + "prevId": "b4eeeb5b-524b-4e3a-bdbc-0ad114343a8f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "companies_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "taxID": { + "name": "taxID", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contract_envelopes": { + "name": "contract_envelopes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "contract_envelopes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "contract_id": { + "name": "contract_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "envelope_id": { + "name": "envelope_id", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "envelope_status": { + "name": "envelope_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "contract_envelopes_contract_id_contracts_id_fk": { + "name": "contract_envelopes_contract_id_contracts_id_fk", + "tableFrom": "contract_envelopes", + "tableTo": "contracts", + "columnsFrom": [ + "contract_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contract_items": { + "name": "contract_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "contract_items_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "contract_id": { + "name": "contract_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "unit_price": { + "name": "unit_price", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "tax_rate": { + "name": "tax_rate", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "tax_amount": { + "name": "tax_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "total_line_amount": { + "name": "total_line_amount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contract_items_contract_item_idx": { + "name": "contract_items_contract_item_idx", + "columns": [ + { + "expression": "contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contract_items_contract_id_contracts_id_fk": { + "name": "contract_items_contract_id_contracts_id_fk", + "tableFrom": "contract_items", + "tableTo": "contracts", + "columnsFrom": [ + "contract_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contract_items_item_id_items_id_fk": { + "name": "contract_items_item_id_items_id_fk", + "tableFrom": "contract_items", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contract_items_contract_id_item_id_unique": { + "name": "contract_items_contract_id_item_id_unique", + "nullsNotDistinct": false, + "columns": [ + "contract_id", + "item_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contract_signers": { + "name": "contract_signers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "contract_signers_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "envelope_id": { + "name": "envelope_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_contact_id": { + "name": "vendor_contact_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signer_type": { + "name": "signer_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'VENDOR'" + }, + "signer_email": { + "name": "signer_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "signer_name": { + "name": "signer_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "signer_position": { + "name": "signer_position", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "signer_status": { + "name": "signer_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'PENDING'" + }, + "signed_at": { + "name": "signed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "contract_signers_envelope_id_contract_envelopes_id_fk": { + "name": "contract_signers_envelope_id_contract_envelopes_id_fk", + "tableFrom": "contract_signers", + "tableTo": "contract_envelopes", + "columnsFrom": [ + "envelope_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contract_signers_vendor_contact_id_vendor_contacts_id_fk": { + "name": "contract_signers_vendor_contact_id_vendor_contacts_id_fk", + "tableFrom": "contract_signers", + "tableTo": "vendor_contacts", + "columnsFrom": [ + "vendor_contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contracts": { + "name": "contracts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "contracts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "contract_no": { + "name": "contract_no", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "contract_name": { + "name": "contract_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "payment_terms": { + "name": "payment_terms", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_terms": { + "name": "delivery_terms", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "delivery_location": { + "name": "delivery_location", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'KRW'" + }, + "total_amount": { + "name": "total_amount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "discount": { + "name": "discount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "tax": { + "name": "tax", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "shipping_fee": { + "name": "shipping_fee", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "net_total": { + "name": "net_total", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "partial_shipping_allowed": { + "name": "partial_shipping_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "partial_payment_allowed": { + "name": "partial_payment_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "remarks": { + "name": "remarks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "contracts_project_id_projects_id_fk": { + "name": "contracts_project_id_projects_id_fk", + "tableFrom": "contracts", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contracts_vendor_id_vendors_id_fk": { + "name": "contracts_vendor_id_vendors_id_fk", + "tableFrom": "contracts", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contracts_contract_no_unique": { + "name": "contracts_contract_no_unique", + "nullsNotDistinct": false, + "columns": [ + "contract_no" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.poa": { + "name": "poa", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "poa_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "contract_no": { + "name": "contract_no", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "original_contract_no": { + "name": "original_contract_no", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "original_contract_name": { + "name": "original_contract_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "original_status": { + "name": "original_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "delivery_terms": { + "name": "delivery_terms", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "delivery_location": { + "name": "delivery_location", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "total_amount": { + "name": "total_amount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "discount": { + "name": "discount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "tax": { + "name": "tax", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "shipping_fee": { + "name": "shipping_fee", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "net_total": { + "name": "net_total", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "change_reason": { + "name": "change_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approval_status": { + "name": "approval_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'PENDING'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "poa_original_contract_no_contracts_contract_no_fk": { + "name": "poa_original_contract_no_contracts_contract_no_fk", + "tableFrom": "poa", + "tableTo": "contracts", + "columnsFrom": [ + "original_contract_no" + ], + "columnsTo": [ + "contract_no" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "poa_project_id_projects_id_fk": { + "name": "poa_project_id_projects_id_fk", + "tableFrom": "poa", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "poa_vendor_id_vendors_id_fk": { + "name": "poa_vendor_id_vendors_id_fk", + "tableFrom": "poa", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.item_offshore_hull": { + "name": "item_offshore_hull", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "work_type": { + "name": "work_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "item_list": { + "name": "item_list", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub_item_list": { + "name": "sub_item_list", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.item_offshore_top": { + "name": "item_offshore_top", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "work_type": { + "name": "work_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "item_list": { + "name": "item_list", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub_item_list": { + "name": "sub_item_list", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.item_shipbuilding": { + "name": "item_shipbuilding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "work_type": { + "name": "work_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "item_list": { + "name": "item_list", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ship_types": { + "name": "ship_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'OPTION'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.items": { + "name": "items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_no": { + "name": "project_no", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "item_name": { + "name": "item_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "package_code": { + "name": "package_code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "sm_code": { + "name": "sm_code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_item_code": { + "name": "parent_item_code", + "type": "varchar(18)", + "primaryKey": false, + "notNull": false + }, + "item_level": { + "name": "item_level", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "delete_flag": { + "name": "delete_flag", + "type": "varchar(1)", + "primaryKey": false, + "notNull": false + }, + "unit_of_measure": { + "name": "unit_of_measure", + "type": "varchar(3)", + "primaryKey": false, + "notNull": false + }, + "steel_type": { + "name": "steel_type", + "type": "varchar(2)", + "primaryKey": false, + "notNull": false + }, + "grade_material": { + "name": "grade_material", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "change_date": { + "name": "change_date", + "type": "varchar(8)", + "primaryKey": false, + "notNull": false + }, + "base_unit_of_measure": { + "name": "base_unit_of_measure", + "type": "varchar(3)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "items_item_code_unique": { + "name": "items_item_code_unique", + "nullsNotDistinct": false, + "columns": [ + "item_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.materials": { + "name": "materials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "item_name": { + "name": "item_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_item_code": { + "name": "parent_item_code", + "type": "varchar(18)", + "primaryKey": false, + "notNull": false + }, + "item_level": { + "name": "item_level", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "delete_flag": { + "name": "delete_flag", + "type": "varchar(1)", + "primaryKey": false, + "notNull": false + }, + "unit_of_measure": { + "name": "unit_of_measure", + "type": "varchar(3)", + "primaryKey": false, + "notNull": false + }, + "steel_type": { + "name": "steel_type", + "type": "varchar(2)", + "primaryKey": false, + "notNull": false + }, + "grade_material": { + "name": "grade_material", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "change_date": { + "name": "change_date", + "type": "varchar(8)", + "primaryKey": false, + "notNull": false + }, + "base_unit_of_measure": { + "name": "base_unit_of_measure", + "type": "varchar(3)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "materials_item_code_unique": { + "name": "materials_item_code_unique", + "nullsNotDistinct": false, + "columns": [ + "item_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pq_criterias": { + "name": "pq_criterias", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "check_point": { + "name": "check_point", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remarks": { + "name": "remarks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pq_criterias_extension": { + "name": "pq_criterias_extension", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pq_criteria_id": { + "name": "pq_criteria_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "contract_info": { + "name": "contract_info", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additional_requirement": { + "name": "additional_requirement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pq_criterias_extension_pq_criteria_id_pq_criterias_id_fk": { + "name": "pq_criterias_extension_pq_criteria_id_pq_criterias_id_fk", + "tableFrom": "pq_criterias_extension", + "tableTo": "pq_criterias", + "columnsFrom": [ + "pq_criteria_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "pq_criterias_extension_project_id_projects_id_fk": { + "name": "pq_criterias_extension_project_id_projects_id_fk", + "tableFrom": "pq_criterias_extension", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_criteria_attachments": { + "name": "vendor_criteria_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_criteria_answer_id": { + "name": "vendor_criteria_answer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_criteria_attachments_vendor_criteria_answer_id_vendor_pq_criteria_answers_id_fk": { + "name": "vendor_criteria_attachments_vendor_criteria_answer_id_vendor_pq_criteria_answers_id_fk", + "tableFrom": "vendor_criteria_attachments", + "tableTo": "vendor_pq_criteria_answers", + "columnsFrom": [ + "vendor_criteria_answer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_investigation_attachments": { + "name": "vendor_investigation_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "investigation_id": { + "name": "investigation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "attachment_type": { + "name": "attachment_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'REPORT'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_investigation_attachments_investigation_id_vendor_investigations_id_fk": { + "name": "vendor_investigation_attachments_investigation_id_vendor_investigations_id_fk", + "tableFrom": "vendor_investigation_attachments", + "tableTo": "vendor_investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_investigations": { + "name": "vendor_investigations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pq_submission_id": { + "name": "pq_submission_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "requester_id": { + "name": "requester_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "qm_manager_id": { + "name": "qm_manager_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "investigation_status": { + "name": "investigation_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'PLANNED'" + }, + "evaluation_type": { + "name": "evaluation_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "investigation_address": { + "name": "investigation_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "investigation_method": { + "name": "investigation_method", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "scheduled_start_at": { + "name": "scheduled_start_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scheduled_end_at": { + "name": "scheduled_end_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forecasted_at": { + "name": "forecasted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "evaluation_score": { + "name": "evaluation_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "evaluation_result": { + "name": "evaluation_result", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "investigation_notes": { + "name": "investigation_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_investigations_vendor_id_vendors_id_fk": { + "name": "vendor_investigations_vendor_id_vendors_id_fk", + "tableFrom": "vendor_investigations", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vendor_investigations_pq_submission_id_vendor_pq_submissions_id_fk": { + "name": "vendor_investigations_pq_submission_id_vendor_pq_submissions_id_fk", + "tableFrom": "vendor_investigations", + "tableTo": "vendor_pq_submissions", + "columnsFrom": [ + "pq_submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "vendor_investigations_requester_id_users_id_fk": { + "name": "vendor_investigations_requester_id_users_id_fk", + "tableFrom": "vendor_investigations", + "tableTo": "users", + "columnsFrom": [ + "requester_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vendor_investigations_qm_manager_id_users_id_fk": { + "name": "vendor_investigations_qm_manager_id_users_id_fk", + "tableFrom": "vendor_investigations", + "tableTo": "users", + "columnsFrom": [ + "qm_manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_pq_submissions": { + "name": "vendor_pq_submissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pq_number": { + "name": "pq_number", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "requester_id": { + "name": "requester_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'REQUESTED'" + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reject_reason": { + "name": "reject_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_pq_submission": { + "name": "unique_pq_submission", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vendor_pq_submissions_requester_id_users_id_fk": { + "name": "vendor_pq_submissions_requester_id_users_id_fk", + "tableFrom": "vendor_pq_submissions", + "tableTo": "users", + "columnsFrom": [ + "requester_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vendor_pq_submissions_vendor_id_vendors_id_fk": { + "name": "vendor_pq_submissions_vendor_id_vendors_id_fk", + "tableFrom": "vendor_pq_submissions", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "vendor_pq_submissions_project_id_projects_id_fk": { + "name": "vendor_pq_submissions_project_id_projects_id_fk", + "tableFrom": "vendor_pq_submissions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vendor_pq_submissions_pq_number_unique": { + "name": "vendor_pq_submissions_pq_number_unique", + "nullsNotDistinct": false, + "columns": [ + "pq_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_pq_criteria_answers": { + "name": "vendor_pq_criteria_answers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "criteria_id": { + "name": "criteria_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "answer": { + "name": "answer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_pq_criteria_answers_vendor_id_vendors_id_fk": { + "name": "vendor_pq_criteria_answers_vendor_id_vendors_id_fk", + "tableFrom": "vendor_pq_criteria_answers", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "vendor_pq_criteria_answers_criteria_id_pq_criterias_id_fk": { + "name": "vendor_pq_criteria_answers_criteria_id_pq_criterias_id_fk", + "tableFrom": "vendor_pq_criteria_answers", + "tableTo": "pq_criterias", + "columnsFrom": [ + "criteria_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "vendor_pq_criteria_answers_project_id_projects_id_fk": { + "name": "vendor_pq_criteria_answers_project_id_projects_id_fk", + "tableFrom": "vendor_pq_criteria_answers", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_pq_review_logs": { + "name": "vendor_pq_review_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_pq_criteria_answer_id": { + "name": "vendor_pq_criteria_answer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reviewer_comment": { + "name": "reviewer_comment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reviewer_name": { + "name": "reviewer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_pq_review_logs_vendor_pq_criteria_answer_id_vendor_pq_criteria_answers_id_fk": { + "name": "vendor_pq_review_logs_vendor_pq_criteria_answer_id_vendor_pq_criteria_answers_id_fk", + "tableFrom": "vendor_pq_review_logs", + "tableTo": "vendor_pq_criteria_answers", + "columnsFrom": [ + "vendor_pq_criteria_answer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_project_pqs": { + "name": "vendor_project_pqs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'REQUESTED'" + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reject_reason": { + "name": "reject_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_project_pqs_vendor_id_vendors_id_fk": { + "name": "vendor_project_pqs_vendor_id_vendors_id_fk", + "tableFrom": "vendor_project_pqs", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "vendor_project_pqs_project_id_projects_id_fk": { + "name": "vendor_project_pqs_project_id_projects_id_fk", + "tableFrom": "vendor_project_pqs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bidding_projects": { + "name": "bidding_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pspid": { + "name": "pspid", + "type": "char(24)", + "primaryKey": false, + "notNull": true + }, + "proj_nm": { + "name": "proj_nm", + "type": "varchar(90)", + "primaryKey": false, + "notNull": false + }, + "sector": { + "name": "sector", + "type": "char(1)", + "primaryKey": false, + "notNull": false + }, + "proj_msrm": { + "name": "proj_msrm", + "type": "numeric(3, 0)", + "primaryKey": false, + "notNull": false + }, + "kunnr": { + "name": "kunnr", + "type": "char(10)", + "primaryKey": false, + "notNull": false + }, + "kunnr_nm": { + "name": "kunnr_nm", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "cls_1": { + "name": "cls_1", + "type": "char(10)", + "primaryKey": false, + "notNull": false + }, + "cls1_nm": { + "name": "cls1_nm", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "ptype": { + "name": "ptype", + "type": "char(3)", + "primaryKey": false, + "notNull": false + }, + "ptype_nm": { + "name": "ptype_nm", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "pmodel_cd": { + "name": "pmodel_cd", + "type": "char(10)", + "primaryKey": false, + "notNull": false + }, + "pmodel_nm": { + "name": "pmodel_nm", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "pmodel_sz": { + "name": "pmodel_sz", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "pmodel_uom": { + "name": "pmodel_uom", + "type": "char(5)", + "primaryKey": false, + "notNull": false + }, + "txt04": { + "name": "txt04", + "type": "char(4)", + "primaryKey": false, + "notNull": false + }, + "txt30": { + "name": "txt30", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "estm_pm": { + "name": "estm_pm", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "bidding_projects_pspid_unique": { + "name": "bidding_projects_pspid_unique", + "nullsNotDistinct": false, + "columns": [ + "pspid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_series": { + "name": "project_series", + "schema": "", + "columns": { + "pspid": { + "name": "pspid", + "type": "char(24)", + "primaryKey": false, + "notNull": true + }, + "sers_no": { + "name": "sers_no", + "type": "char(3)", + "primaryKey": false, + "notNull": true + }, + "sc_dt": { + "name": "sc_dt", + "type": "char(8)", + "primaryKey": false, + "notNull": false + }, + "kl_dt": { + "name": "kl_dt", + "type": "char(8)", + "primaryKey": false, + "notNull": false + }, + "lc_dt": { + "name": "lc_dt", + "type": "char(8)", + "primaryKey": false, + "notNull": false + }, + "dl_dt": { + "name": "dl_dt", + "type": "char(8)", + "primaryKey": false, + "notNull": false + }, + "dock_no": { + "name": "dock_no", + "type": "char(3)", + "primaryKey": false, + "notNull": false + }, + "dock_nm": { + "name": "dock_nm", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "proj_no": { + "name": "proj_no", + "type": "char(24)", + "primaryKey": false, + "notNull": false + }, + "post1": { + "name": "post1", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "project_sersNo_unique": { + "name": "project_sersNo_unique", + "columns": [ + { + "expression": "pspid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sers_no", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_series_pspid_bidding_projects_pspid_fk": { + "name": "project_series_pspid_bidding_projects_pspid_fk", + "tableFrom": "project_series", + "tableTo": "bidding_projects", + "columnsFrom": [ + "pspid" + ], + "columnsTo": [ + "pspid" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'ship'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cbe_evaluations": { + "name": "cbe_evaluations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "evaluated_by": { + "name": "evaluated_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "evaluated_at": { + "name": "evaluated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "result": { + "name": "result", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "total_cost": { + "name": "total_cost", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'USD'" + }, + "payment_terms": { + "name": "payment_terms", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "incoterms": { + "name": "incoterms", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "delivery_schedule": { + "name": "delivery_schedule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cbe_evaluations_rfq_id_rfqs_id_fk": { + "name": "cbe_evaluations_rfq_id_rfqs_id_fk", + "tableFrom": "cbe_evaluations", + "tableTo": "rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cbe_evaluations_vendor_id_vendors_id_fk": { + "name": "cbe_evaluations_vendor_id_vendors_id_fk", + "tableFrom": "cbe_evaluations", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cbe_evaluations_evaluated_by_users_id_fk": { + "name": "cbe_evaluations_evaluated_by_users_id_fk", + "tableFrom": "cbe_evaluations", + "tableTo": "users", + "columnsFrom": [ + "evaluated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rfq_attachments": { + "name": "rfq_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "evaluation_id": { + "name": "evaluation_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cbe_id": { + "name": "cbe_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "rfq_attachments_rfq_id_rfqs_id_fk": { + "name": "rfq_attachments_rfq_id_rfqs_id_fk", + "tableFrom": "rfq_attachments", + "tableTo": "rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rfq_attachments_vendor_id_vendors_id_fk": { + "name": "rfq_attachments_vendor_id_vendors_id_fk", + "tableFrom": "rfq_attachments", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rfq_attachments_evaluation_id_rfq_evaluations_id_fk": { + "name": "rfq_attachments_evaluation_id_rfq_evaluations_id_fk", + "tableFrom": "rfq_attachments", + "tableTo": "rfq_evaluations", + "columnsFrom": [ + "evaluation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rfq_attachments_cbe_id_cbe_evaluations_id_fk": { + "name": "rfq_attachments_cbe_id_cbe_evaluations_id_fk", + "tableFrom": "rfq_attachments", + "tableTo": "cbe_evaluations", + "columnsFrom": [ + "cbe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rfq_attachments_comment_id_rfq_comments_id_fk": { + "name": "rfq_attachments_comment_id_rfq_comments_id_fk", + "tableFrom": "rfq_attachments", + "tableTo": "rfq_comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rfq_comments": { + "name": "rfq_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "comment_text": { + "name": "comment_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commented_by": { + "name": "commented_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "evaluation_id": { + "name": "evaluation_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cbe_id": { + "name": "cbe_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "rfq_comments_rfq_id_rfqs_id_fk": { + "name": "rfq_comments_rfq_id_rfqs_id_fk", + "tableFrom": "rfq_comments", + "tableTo": "rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rfq_comments_vendor_id_vendors_id_fk": { + "name": "rfq_comments_vendor_id_vendors_id_fk", + "tableFrom": "rfq_comments", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rfq_comments_commented_by_users_id_fk": { + "name": "rfq_comments_commented_by_users_id_fk", + "tableFrom": "rfq_comments", + "tableTo": "users", + "columnsFrom": [ + "commented_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rfq_comments_evaluation_id_rfq_evaluations_id_fk": { + "name": "rfq_comments_evaluation_id_rfq_evaluations_id_fk", + "tableFrom": "rfq_comments", + "tableTo": "rfq_evaluations", + "columnsFrom": [ + "evaluation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rfq_comments_cbe_id_vendor_responses_id_fk": { + "name": "rfq_comments_cbe_id_vendor_responses_id_fk", + "tableFrom": "rfq_comments", + "tableTo": "vendor_responses", + "columnsFrom": [ + "cbe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rfq_evaluations": { + "name": "rfq_evaluations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "eval_type": { + "name": "eval_type", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "rfq_evaluations_rfq_id_rfqs_id_fk": { + "name": "rfq_evaluations_rfq_id_rfqs_id_fk", + "tableFrom": "rfq_evaluations", + "tableTo": "rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rfq_evaluations_vendor_id_vendors_id_fk": { + "name": "rfq_evaluations_vendor_id_vendors_id_fk", + "tableFrom": "rfq_evaluations", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rfq_items": { + "name": "rfq_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "uom": { + "name": "uom", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "rfq_items_rfq_id_rfqs_id_fk": { + "name": "rfq_items_rfq_id_rfqs_id_fk", + "tableFrom": "rfq_items", + "tableTo": "rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rfq_items_item_code_items_item_code_fk": { + "name": "rfq_items_item_code_items_item_code_fk", + "tableFrom": "rfq_items", + "tableTo": "items", + "columnsFrom": [ + "item_code" + ], + "columnsTo": [ + "item_code" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rfqs": { + "name": "rfqs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bid_project_id": { + "name": "bid_project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "rfq_type": { + "name": "rfq_type", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "default": "'PURCHASE'" + }, + "parent_rfq_id": { + "name": "parent_rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "rfqs_project_id_projects_id_fk": { + "name": "rfqs_project_id_projects_id_fk", + "tableFrom": "rfqs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "rfqs_bid_project_id_bidding_projects_id_fk": { + "name": "rfqs_bid_project_id_bidding_projects_id_fk", + "tableFrom": "rfqs", + "tableTo": "bidding_projects", + "columnsFrom": [ + "bid_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "rfqs_created_by_users_id_fk": { + "name": "rfqs_created_by_users_id_fk", + "tableFrom": "rfqs", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "rfqs_parent_rfq_id_rfqs_id_fk": { + "name": "rfqs_parent_rfq_id_rfqs_id_fk", + "tableFrom": "rfqs", + "tableTo": "rfqs", + "columnsFrom": [ + "parent_rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rfqs_rfq_code_unique": { + "name": "rfqs_rfq_code_unique", + "nullsNotDistinct": false, + "columns": [ + "rfq_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_commercial_responses": { + "name": "vendor_commercial_responses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "response_id": { + "name": "response_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "response_status": { + "name": "response_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "total_price": { + "name": "total_price", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'USD'" + }, + "payment_terms": { + "name": "payment_terms", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "incoterms": { + "name": "incoterms", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "delivery_period": { + "name": "delivery_period", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "warranty_period": { + "name": "warranty_period", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "validity_period": { + "name": "validity_period", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "price_breakdown": { + "name": "price_breakdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "commercial_notes": { + "name": "commercial_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_commercial_responses_response_id_vendor_responses_id_fk": { + "name": "vendor_commercial_responses_response_id_vendor_responses_id_fk", + "tableFrom": "vendor_commercial_responses", + "tableTo": "vendor_responses", + "columnsFrom": [ + "response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_response_attachments": { + "name": "vendor_response_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "response_id": { + "name": "response_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "technical_response_id": { + "name": "technical_response_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commercial_response_id": { + "name": "commercial_response_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "attachment_type": { + "name": "attachment_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_response_attachments_response_id_vendor_responses_id_fk": { + "name": "vendor_response_attachments_response_id_vendor_responses_id_fk", + "tableFrom": "vendor_response_attachments", + "tableTo": "vendor_responses", + "columnsFrom": [ + "response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vendor_response_attachments_technical_response_id_vendor_technical_responses_id_fk": { + "name": "vendor_response_attachments_technical_response_id_vendor_technical_responses_id_fk", + "tableFrom": "vendor_response_attachments", + "tableTo": "vendor_technical_responses", + "columnsFrom": [ + "technical_response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vendor_response_attachments_commercial_response_id_vendor_commercial_responses_id_fk": { + "name": "vendor_response_attachments_commercial_response_id_vendor_commercial_responses_id_fk", + "tableFrom": "vendor_response_attachments", + "tableTo": "vendor_commercial_responses", + "columnsFrom": [ + "commercial_response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_responses": { + "name": "vendor_responses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "response_status": { + "name": "response_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'REVIEWING'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "responded_by": { + "name": "responded_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "responded_at": { + "name": "responded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "vendor_response_unique": { + "name": "vendor_response_unique", + "columns": [ + { + "expression": "rfq_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vendor_responses_rfq_id_rfqs_id_fk": { + "name": "vendor_responses_rfq_id_rfqs_id_fk", + "tableFrom": "vendor_responses", + "tableTo": "rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vendor_responses_vendor_id_vendors_id_fk": { + "name": "vendor_responses_vendor_id_vendors_id_fk", + "tableFrom": "vendor_responses", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_technical_responses": { + "name": "vendor_technical_responses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "response_id": { + "name": "response_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "response_status": { + "name": "response_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_technical_responses_response_id_vendor_responses_id_fk": { + "name": "vendor_technical_responses_response_id_vendor_responses_id_fk", + "tableFrom": "vendor_technical_responses", + "tableTo": "vendor_responses", + "columnsFrom": [ + "response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.departments": { + "name": "departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "department_code": { + "name": "department_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "department_name": { + "name": "department_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "departments_department_code_unique": { + "name": "departments_department_code_unique", + "nullsNotDistinct": false, + "columns": [ + "department_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.otps": { + "name": "otps", + "schema": "", + "columns": { + "email": { + "name": "email", + "type": "varchar(256)", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(6)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "otpToken": { + "name": "otpToken", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "otp_expires": { + "name": "otp_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "permissions_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permissions": { + "name": "role_permissions", + "schema": "", + "columns": { + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "permission_id": { + "name": "permission_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "role_permissions_role_id_roles_id_fk": { + "name": "role_permissions_role_id_roles_id_fk", + "tableFrom": "role_permissions", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_permissions_permission_id_permissions_id_fk": { + "name": "role_permissions_permission_id_permissions_id_fk", + "tableFrom": "role_permissions", + "tableTo": "permissions", + "columnsFrom": [ + "permission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "roles_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "user_domain", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "roles_company_id_vendors_id_fk": { + "name": "roles_company_id_vendors_id_fk", + "tableFrom": "roles", + "tableTo": "vendors", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tech_company_id": { + "name": "tech_company_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "user_domain", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'partners'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "image_url": { + "name": "image_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'en'" + } + }, + "indexes": {}, + "foreignKeys": { + "users_company_id_vendors_id_fk": { + "name": "users_company_id_vendors_id_fk", + "tableFrom": "users", + "tableTo": "vendors", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_tech_company_id_tech_vendors_id_fk": { + "name": "users_tech_company_id_tech_vendors_id_fk", + "tableFrom": "users", + "tableTo": "tech_vendors", + "columnsFrom": [ + "tech_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_entries": { + "name": "form_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "form_code": { + "name": "form_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "contract_item_id": { + "name": "contract_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "form_entries_contract_item_id_contract_items_id_fk": { + "name": "form_entries_contract_item_id_contract_items_id_fk", + "tableFrom": "form_entries", + "tableTo": "contract_items", + "columnsFrom": [ + "contract_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_metas": { + "name": "form_metas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "form_code": { + "name": "form_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "form_name": { + "name": "form_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "columns": { + "name": "columns", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "form_metas_project_id_projects_id_fk": { + "name": "form_metas_project_id_projects_id_fk", + "tableFrom": "form_metas", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "form_code_project_unique": { + "name": "form_code_project_unique", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "form_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.forms": { + "name": "forms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "forms_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "contract_item_id": { + "name": "contract_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "form_code": { + "name": "form_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "form_name": { + "name": "form_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "eng": { + "name": "eng", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "im": { + "name": "im", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contract_item_form_code_unique": { + "name": "contract_item_form_code_unique", + "columns": [ + { + "expression": "contract_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "form_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "forms_contract_item_id_contract_items_id_fk": { + "name": "forms_contract_item_id_contract_items_id_fk", + "tableFrom": "forms", + "tableTo": "contract_items", + "columnsFrom": [ + "contract_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag_class_attributes": { + "name": "tag_class_attributes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "tag_class_attributes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "tag_class_id": { + "name": "tag_class_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "att_id": { + "name": "att_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "def_val": { + "name": "def_val", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uom_id": { + "name": "uom_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tag_class_attributes_seq_idx": { + "name": "tag_class_attributes_seq_idx", + "columns": [ + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tag_class_attributes_tag_class_id_tag_classes_id_fk": { + "name": "tag_class_attributes_tag_class_id_tag_classes_id_fk", + "tableFrom": "tag_class_attributes", + "tableTo": "tag_classes", + "columnsFrom": [ + "tag_class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uniq_att_id_in_tag_class": { + "name": "uniq_att_id_in_tag_class", + "nullsNotDistinct": false, + "columns": [ + "tag_class_id", + "att_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag_classes": { + "name": "tag_classes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "tag_classes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_type_code": { + "name": "tag_type_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tag_classes_project_id_projects_id_fk": { + "name": "tag_classes_project_id_projects_id_fk", + "tableFrom": "tag_classes", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tag_classes_tag_type_code_project_id_tag_types_code_project_id_fk": { + "name": "tag_classes_tag_type_code_project_id_tag_types_code_project_id_fk", + "tableFrom": "tag_classes", + "tableTo": "tag_types", + "columnsFrom": [ + "tag_type_code", + "project_id" + ], + "columnsTo": [ + "code", + "project_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uniq_code_in_project": { + "name": "uniq_code_in_project", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag_subfield_options": { + "name": "tag_subfield_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "attributes_id": { + "name": "attributes_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tag_subfield_options_project_id_projects_id_fk": { + "name": "tag_subfield_options_project_id_projects_id_fk", + "tableFrom": "tag_subfield_options", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uniq_attribute_project_code": { + "name": "uniq_attribute_project_code", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "attributes_id", + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag_subfields": { + "name": "tag_subfields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag_type_code": { + "name": "tag_type_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "attributes_id": { + "name": "attributes_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "attributes_description": { + "name": "attributes_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expression": { + "name": "expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delimiter": { + "name": "delimiter", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tag_subfields_project_id_projects_id_fk": { + "name": "tag_subfields_project_id_projects_id_fk", + "tableFrom": "tag_subfields", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uniq_tag_type_attribute": { + "name": "uniq_tag_type_attribute", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "tag_type_code", + "attributes_id" + ] + }, + "uniq_attribute_id_project": { + "name": "uniq_attribute_id_project", + "nullsNotDistinct": false, + "columns": [ + "attributes_id", + "project_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag_type_class_form_mappings": { + "name": "tag_type_class_form_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag_type_label": { + "name": "tag_type_label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "class_label": { + "name": "class_label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "form_code": { + "name": "form_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "form_name": { + "name": "form_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "ep": { + "name": "ep", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "remark": { + "name": "remark", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uniq_mapping_in_project": { + "name": "uniq_mapping_in_project", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "tag_type_label", + "class_label", + "form_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag_types": { + "name": "tag_types", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tag_types_project_id_projects_id_fk": { + "name": "tag_types_project_id_projects_id_fk", + "tableFrom": "tag_types", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tag_types_code_project_id_pk": { + "name": "tag_types_code_project_id_pk", + "columns": [ + "code", + "project_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "tags_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "contract_item_id": { + "name": "contract_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "form_id": { + "name": "form_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tag_no": { + "name": "tag_no", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "tag_type": { + "name": "tag_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "class": { + "name": "class", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tags_contract_item_id_contract_items_id_fk": { + "name": "tags_contract_item_id_contract_items_id_fk", + "tableFrom": "tags", + "tableTo": "contract_items", + "columnsFrom": [ + "contract_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tags_form_id_forms_id_fk": { + "name": "tags_form_id_forms_id_fk", + "tableFrom": "tags", + "tableTo": "forms", + "columnsFrom": [ + "form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contract_item_tag_no_unique": { + "name": "contract_item_tag_no_unique", + "nullsNotDistinct": false, + "columns": [ + "contract_item_id", + "tag_no" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_data_report_temps": { + "name": "vendor_data_report_temps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contract_item_id": { + "name": "contract_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "form_id": { + "name": "form_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_data_report_temps_contract_item_id_contract_items_id_fk": { + "name": "vendor_data_report_temps_contract_item_id_contract_items_id_fk", + "tableFrom": "vendor_data_report_temps", + "tableTo": "contract_items", + "columnsFrom": [ + "contract_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vendor_data_report_temps_form_id_forms_id_fk": { + "name": "vendor_data_report_temps_form_id_forms_id_fk", + "tableFrom": "vendor_data_report_temps", + "tableTo": "forms", + "columnsFrom": [ + "form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.change_logs": { + "name": "change_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contract_id": { + "name": "contract_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "changed_fields": { + "name": "changed_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "old_values": { + "name": "old_values", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "new_values": { + "name": "new_values", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_name": { + "name": "user_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_synced": { + "name": "is_synced", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sync_attempts": { + "name": "sync_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "target_systems": { + "name": "target_systems", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "idx_change_logs_contract_synced": { + "name": "idx_change_logs_contract_synced", + "columns": [ + { + "expression": "contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_synced", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_change_logs_created_at": { + "name": "idx_change_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_change_logs_entity": { + "name": "idx_change_logs_entity", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_change_logs_sync_attempts": { + "name": "idx_change_logs_sync_attempts", + "columns": [ + { + "expression": "sync_attempts", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_attachments": { + "name": "document_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "document_attachments_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "revision_id": { + "name": "revision_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "upload_id": { + "name": "upload_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": false + }, + "file_id": { + "name": "file_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "dolce_file_path": { + "name": "dolce_file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_attachments_revision_id_revisions_id_fk": { + "name": "document_attachments_revision_id_revisions_id_fk", + "tableFrom": "document_attachments", + "tableTo": "revisions", + "columnsFrom": [ + "revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "documents_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "pic": { + "name": "pic", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "contract_id": { + "name": "contract_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "doc_number": { + "name": "doc_number", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "vendor_doc_number": { + "name": "vendor_doc_number", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "issued_date": { + "name": "issued_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "drawing_kind": { + "name": "drawing_kind", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "drawing_move_gbn": { + "name": "drawing_move_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "discipline": { + "name": "discipline", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "external_document_id": { + "name": "external_document_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "external_system_type": { + "name": "external_system_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "external_synced_at": { + "name": "external_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "c_gbn": { + "name": "c_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "d_gbn": { + "name": "d_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "degree_gbn": { + "name": "degree_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "dept_gbn": { + "name": "dept_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "j_gbn": { + "name": "j_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "s_gbn": { + "name": "s_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "shi_drawing_no": { + "name": "shi_drawing_no", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "manager": { + "name": "manager", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "manager_enm": { + "name": "manager_enm", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "manager_no": { + "name": "manager_no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "register_group": { + "name": "register_group", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "register_group_id": { + "name": "register_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "create_user_no": { + "name": "create_user_no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "create_user_id": { + "name": "create_user_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "create_user_enm": { + "name": "create_user_enm", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_contract_doc_status": { + "name": "unique_contract_doc_status", + "columns": [ + { + "expression": "contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "doc_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_contract_vendor_doc": { + "name": "unique_contract_vendor_doc", + "columns": [ + { + "expression": "contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor_doc_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"documents\".\"vendor_doc_number\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_external_doc": { + "name": "unique_external_doc", + "columns": [ + { + "expression": "contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_system_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"documents\".\"external_document_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "drawing_kind_idx": { + "name": "drawing_kind_idx", + "columns": [ + { + "expression": "drawing_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_contract_id_contracts_id_fk": { + "name": "documents_contract_id_contracts_id_fk", + "tableFrom": "documents", + "tableTo": "contracts", + "columnsFrom": [ + "contract_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_stages": { + "name": "issue_stages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "issue_stages_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "document_id": { + "name": "document_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "stage_name": { + "name": "stage_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "plan_date": { + "name": "plan_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "actual_date": { + "name": "actual_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "stage_status": { + "name": "stage_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'PLANNED'" + }, + "stage_order": { + "name": "stage_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "priority": { + "name": "priority", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'MEDIUM'" + }, + "assignee_id": { + "name": "assignee_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "assignee_name": { + "name": "assignee_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "reminder_days": { + "name": "reminder_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_document_stage": { + "name": "unique_document_stage", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_stage_order": { + "name": "document_stage_order", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_stages_document_id_documents_id_fk": { + "name": "issue_stages_document_id_documents_id_fk", + "tableFrom": "issue_stages", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.revisions": { + "name": "revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "revisions_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "issue_stage_id": { + "name": "issue_stage_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "uploader_type": { + "name": "uploader_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'vendor'" + }, + "uploader_id": { + "name": "uploader_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploader_name": { + "name": "uploader_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "usage": { + "name": "usage", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "usage_type": { + "name": "usage_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "revision_status": { + "name": "revision_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'SUBMITTED'" + }, + "submitted_date": { + "name": "submitted_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "review_start_date": { + "name": "review_start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "approved_date": { + "name": "approved_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "rejected_date": { + "name": "rejected_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "reviewer_id": { + "name": "reviewer_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reviewer_name": { + "name": "reviewer_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "review_comments": { + "name": "review_comments", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false + }, + "external_upload_id": { + "name": "external_upload_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_stage_revision_usage": { + "name": "unique_stage_revision_usage", + "columns": [ + { + "expression": "issue_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "COALESCE(\"usage_type\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_batches": { + "name": "sync_batches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contract_id": { + "name": "contract_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_system": { + "name": "target_system", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "batch_size": { + "name": "batch_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "change_log_ids": { + "name": "change_log_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "success_count": { + "name": "success_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "failure_count": { + "name": "failure_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_metadata": { + "name": "sync_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_sync_batches_contract_system": { + "name": "idx_sync_batches_contract_system", + "columns": [ + { + "expression": "contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_system", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_batches_status": { + "name": "idx_sync_batches_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_batches_created_at": { + "name": "idx_sync_batches_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_configs": { + "name": "sync_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contract_id": { + "name": "contract_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_system": { + "name": "target_system", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "sync_enabled": { + "name": "sync_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "last_successful_sync": { + "name": "last_successful_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_attempt": { + "name": "last_sync_attempt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "endpoint_url": { + "name": "endpoint_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_token": { + "name": "auth_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_version": { + "name": "api_version", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'v1'" + }, + "max_batch_size": { + "name": "max_batch_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 100 + }, + "retry_max_attempts": { + "name": "retry_max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_sync_configs_contract_system": { + "name": "idx_sync_configs_contract_system", + "columns": [ + { + "expression": "contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_system", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_configs_contract_id_contracts_id_fk": { + "name": "sync_configs_contract_id_contracts_id_fk", + "tableFrom": "sync_configs", + "tableTo": "contracts", + "columnsFrom": [ + "contract_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_attachments": { + "name": "vendor_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "attachment_type": { + "name": "attachment_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'GENERAL'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_attachments_vendor_id_vendors_id_fk": { + "name": "vendor_attachments_vendor_id_vendors_id_fk", + "tableFrom": "vendor_attachments", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_candidates": { + "name": "vendor_candidates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "company_name": { + "name": "company_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "contact_email": { + "name": "contact_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "contact_phone": { + "name": "contact_phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'COLLECTED'" + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "items": { + "name": "items", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_candidates_vendor_id_vendors_id_fk": { + "name": "vendor_candidates_vendor_id_vendors_id_fk", + "tableFrom": "vendor_candidates", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_contacts": { + "name": "vendor_contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "contact_name": { + "name": "contact_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "contact_position": { + "name": "contact_position", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "contact_email": { + "name": "contact_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "contact_phone": { + "name": "contact_phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_contacts_vendor_id_vendors_id_fk": { + "name": "vendor_contacts_vendor_id_vendors_id_fk", + "tableFrom": "vendor_contacts", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_possible_items": { + "name": "vendor_possible_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_possible_items_vendor_id_vendors_id_fk": { + "name": "vendor_possible_items_vendor_id_vendors_id_fk", + "tableFrom": "vendor_possible_items", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vendor_possible_items_item_code_items_item_code_fk": { + "name": "vendor_possible_items_item_code_items_item_code_fk", + "tableFrom": "vendor_possible_items", + "tableTo": "items", + "columnsFrom": [ + "item_code" + ], + "columnsTo": [ + "item_code" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_possible_materials": { + "name": "vendor_possible_materials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_possible_materials_vendor_id_vendors_id_fk": { + "name": "vendor_possible_materials_vendor_id_vendors_id_fk", + "tableFrom": "vendor_possible_materials", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vendor_possible_materials_item_code_materials_item_code_fk": { + "name": "vendor_possible_materials_item_code_materials_item_code_fk", + "tableFrom": "vendor_possible_materials", + "tableTo": "materials", + "columnsFrom": [ + "item_code" + ], + "columnsTo": [ + "item_code" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_types": { + "name": "vendor_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "name_ko": { + "name": "name_ko", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name_en": { + "name": "name_en", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vendor_types_code_unique": { + "name": "vendor_types_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendors": { + "name": "vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'PENDING_REVIEW'" + }, + "vendor_type_id": { + "name": "vendor_type_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "representative_name": { + "name": "representative_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "representative_birth": { + "name": "representative_birth", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "representative_email": { + "name": "representative_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "representative_phone": { + "name": "representative_phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "corporate_registration_number": { + "name": "corporate_registration_number", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "items": { + "name": "items", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credit_agency": { + "name": "credit_agency", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "credit_rating": { + "name": "credit_rating", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "cash_flow_rating": { + "name": "cash_flow_rating", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "business_size": { + "name": "business_size", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendors_vendor_type_id_vendor_types_id_fk": { + "name": "vendors_vendor_type_id_vendor_types_id_fk", + "tableFrom": "vendors", + "tableTo": "vendor_types", + "columnsFrom": [ + "vendor_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'todo'" + }, + "label": { + "name": "label", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'bug'" + }, + "priority": { + "name": "priority", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'low'" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "current_timestamp" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tasks_code_unique": { + "name": "tasks_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_candidate_logs": { + "name": "vendor_candidate_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_candidate_id": { + "name": "vendor_candidate_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "old_status": { + "name": "old_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "new_status": { + "name": "new_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_candidate_logs_vendor_candidate_id_vendor_candidates_id_fk": { + "name": "vendor_candidate_logs_vendor_candidate_id_vendor_candidates_id_fk", + "tableFrom": "vendor_candidate_logs", + "tableTo": "vendor_candidates", + "columnsFrom": [ + "vendor_candidate_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vendor_candidate_logs_user_id_users_id_fk": { + "name": "vendor_candidate_logs_user_id_users_id_fk", + "tableFrom": "vendor_candidate_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendors_logs": { + "name": "vendors_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "old_status": { + "name": "old_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "new_status": { + "name": "new_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendors_logs_vendor_id_vendors_id_fk": { + "name": "vendors_logs_vendor_id_vendors_id_fk", + "tableFrom": "vendors_logs", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vendors_logs_user_id_users_id_fk": { + "name": "vendors_logs_user_id_users_id_fk", + "tableFrom": "vendors_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.basic_contract": { + "name": "basic_contract", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "basic_contract_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "template_id": { + "name": "template_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "requested_by": { + "name": "requested_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "basic_contract_template_id_basic_contract_templates_id_fk": { + "name": "basic_contract_template_id_basic_contract_templates_id_fk", + "tableFrom": "basic_contract", + "tableTo": "basic_contract_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "basic_contract_vendor_id_vendors_id_fk": { + "name": "basic_contract_vendor_id_vendors_id_fk", + "tableFrom": "basic_contract", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "basic_contract_requested_by_users_id_fk": { + "name": "basic_contract_requested_by_users_id_fk", + "tableFrom": "basic_contract", + "tableTo": "users", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.basic_contract_templates": { + "name": "basic_contract_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "basic_contract_templates_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "template_name": { + "name": "template_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "validity_period": { + "name": "validity_period", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.incoterms": { + "name": "incoterms", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "varchar(20)", + "primaryKey": true, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "incoterms_created_by_users_id_fk": { + "name": "incoterms_created_by_users_id_fk", + "tableFrom": "incoterms", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_terms": { + "name": "payment_terms", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": true, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "payment_terms_created_by_users_id_fk": { + "name": "payment_terms_created_by_users_id_fk", + "tableFrom": "payment_terms", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_items": { + "name": "pr_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "procurement_rfqs_id": { + "name": "procurement_rfqs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_item": { + "name": "rfq_item", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "pr_item": { + "name": "pr_item", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "pr_no": { + "name": "pr_no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "material_code": { + "name": "material_code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "material_category": { + "name": "material_category", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "acc": { + "name": "acc", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "material_description": { + "name": "material_description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "uom": { + "name": "uom", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "gross_weight": { + "name": "gross_weight", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "gw_uom": { + "name": "gw_uom", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "spec_no": { + "name": "spec_no", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "spec_url": { + "name": "spec_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tracking_no": { + "name": "tracking_no", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "major_yn": { + "name": "major_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "project_def": { + "name": "project_def", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_sc": { + "name": "project_sc", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_kl": { + "name": "project_kl", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_lc": { + "name": "project_lc", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_dl": { + "name": "project_dl", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "pr_items_procurement_rfqs_id_procurement_rfqs_id_fk": { + "name": "pr_items_procurement_rfqs_id_procurement_rfqs_id_fk", + "tableFrom": "pr_items", + "tableTo": "procurement_rfqs", + "columnsFrom": [ + "procurement_rfqs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.procurement_attachments": { + "name": "procurement_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "attachment_type": { + "name": "attachment_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "procurement_rfqs_id": { + "name": "procurement_rfqs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "procurement_rfq_details_id": { + "name": "procurement_rfq_details_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "original_file_name": { + "name": "original_file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "procurement_attachments_procurement_rfqs_id_procurement_rfqs_id_fk": { + "name": "procurement_attachments_procurement_rfqs_id_procurement_rfqs_id_fk", + "tableFrom": "procurement_attachments", + "tableTo": "procurement_rfqs", + "columnsFrom": [ + "procurement_rfqs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "procurement_attachments_procurement_rfq_details_id_procurement_rfq_details_id_fk": { + "name": "procurement_attachments_procurement_rfq_details_id_procurement_rfq_details_id_fk", + "tableFrom": "procurement_attachments", + "tableTo": "procurement_rfq_details", + "columnsFrom": [ + "procurement_rfq_details_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "procurement_attachments_created_by_users_id_fk": { + "name": "procurement_attachments_created_by_users_id_fk", + "tableFrom": "procurement_attachments", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "attachment_type_check": { + "name": "attachment_type_check", + "value": "\"procurement_attachments\".\"procurement_rfqs_id\" IS NOT NULL OR \"procurement_attachments\".\"procurement_rfq_details_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.procurement_quotation_items": { + "name": "procurement_quotation_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pr_item_id": { + "name": "pr_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material_code": { + "name": "material_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "material_description": { + "name": "material_description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "uom": { + "name": "uom", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "unit_price": { + "name": "unit_price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "total_price": { + "name": "total_price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'USD'" + }, + "vendor_material_code": { + "name": "vendor_material_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "vendor_material_description": { + "name": "vendor_material_description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "lead_time_in_days": { + "name": "lead_time_in_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tax_rate": { + "name": "tax_rate", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "tax_amount": { + "name": "tax_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "discount_rate": { + "name": "discount_rate", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "discount_amount": { + "name": "discount_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_alternative": { + "name": "is_alternative", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_recommended": { + "name": "is_recommended", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "procurement_quotation_items_quotation_id_procurement_vendor_quotations_id_fk": { + "name": "procurement_quotation_items_quotation_id_procurement_vendor_quotations_id_fk", + "tableFrom": "procurement_quotation_items", + "tableTo": "procurement_vendor_quotations", + "columnsFrom": [ + "quotation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "procurement_quotation_items_pr_item_id_pr_items_id_fk": { + "name": "procurement_quotation_items_pr_item_id_pr_items_id_fk", + "tableFrom": "procurement_quotation_items", + "tableTo": "pr_items", + "columnsFrom": [ + "pr_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.procurement_rfq_attachments": { + "name": "procurement_rfq_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "quotation_id": { + "name": "quotation_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "file_path": { + "name": "file_path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "is_vendor_upload": { + "name": "is_vendor_upload", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "procurement_rfq_attachments_rfq_id_procurement_rfqs_id_fk": { + "name": "procurement_rfq_attachments_rfq_id_procurement_rfqs_id_fk", + "tableFrom": "procurement_rfq_attachments", + "tableTo": "procurement_rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "procurement_rfq_attachments_comment_id_procurement_rfq_comments_id_fk": { + "name": "procurement_rfq_attachments_comment_id_procurement_rfq_comments_id_fk", + "tableFrom": "procurement_rfq_attachments", + "tableTo": "procurement_rfq_comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "procurement_rfq_attachments_quotation_id_procurement_vendor_quotations_id_fk": { + "name": "procurement_rfq_attachments_quotation_id_procurement_vendor_quotations_id_fk", + "tableFrom": "procurement_rfq_attachments", + "tableTo": "procurement_vendor_quotations", + "columnsFrom": [ + "quotation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "procurement_rfq_attachments_uploaded_by_users_id_fk": { + "name": "procurement_rfq_attachments_uploaded_by_users_id_fk", + "tableFrom": "procurement_rfq_attachments", + "tableTo": "users", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "procurement_rfq_attachments_vendor_id_vendors_id_fk": { + "name": "procurement_rfq_attachments_vendor_id_vendors_id_fk", + "tableFrom": "procurement_rfq_attachments", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.procurement_rfq_comments": { + "name": "procurement_rfq_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_vendor_comment": { + "name": "is_vendor_comment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_read": { + "name": "is_read", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "procurement_rfq_comments_rfq_id_procurement_rfqs_id_fk": { + "name": "procurement_rfq_comments_rfq_id_procurement_rfqs_id_fk", + "tableFrom": "procurement_rfq_comments", + "tableTo": "procurement_rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "procurement_rfq_comments_vendor_id_vendors_id_fk": { + "name": "procurement_rfq_comments_vendor_id_vendors_id_fk", + "tableFrom": "procurement_rfq_comments", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "procurement_rfq_comments_user_id_users_id_fk": { + "name": "procurement_rfq_comments_user_id_users_id_fk", + "tableFrom": "procurement_rfq_comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "procurement_rfq_comments_parent_comment_id_procurement_rfq_comments_id_fk": { + "name": "procurement_rfq_comments_parent_comment_id_procurement_rfq_comments_id_fk", + "tableFrom": "procurement_rfq_comments", + "tableTo": "procurement_rfq_comments", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.procurement_rfq_details": { + "name": "procurement_rfq_details", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "procurement_rfqs_id": { + "name": "procurement_rfqs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendors_id": { + "name": "vendors_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'USD'" + }, + "payment_terms_code": { + "name": "payment_terms_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "incoterms_code": { + "name": "incoterms_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "incoterms_detail": { + "name": "incoterms_detail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "tax_code": { + "name": "tax_code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "default": "'VV'" + }, + "place_of_shipping": { + "name": "place_of_shipping", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "place_of_destination": { + "name": "place_of_destination", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancel_reason": { + "name": "cancel_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "material_price_related_yn": { + "name": "material_price_related_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "procurement_rfq_details_procurement_rfqs_id_procurement_rfqs_id_fk": { + "name": "procurement_rfq_details_procurement_rfqs_id_procurement_rfqs_id_fk", + "tableFrom": "procurement_rfq_details", + "tableTo": "procurement_rfqs", + "columnsFrom": [ + "procurement_rfqs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "procurement_rfq_details_vendors_id_vendors_id_fk": { + "name": "procurement_rfq_details_vendors_id_vendors_id_fk", + "tableFrom": "procurement_rfq_details", + "tableTo": "vendors", + "columnsFrom": [ + "vendors_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "procurement_rfq_details_payment_terms_code_payment_terms_code_fk": { + "name": "procurement_rfq_details_payment_terms_code_payment_terms_code_fk", + "tableFrom": "procurement_rfq_details", + "tableTo": "payment_terms", + "columnsFrom": [ + "payment_terms_code" + ], + "columnsTo": [ + "code" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "procurement_rfq_details_incoterms_code_incoterms_code_fk": { + "name": "procurement_rfq_details_incoterms_code_incoterms_code_fk", + "tableFrom": "procurement_rfq_details", + "tableTo": "incoterms", + "columnsFrom": [ + "incoterms_code" + ], + "columnsTo": [ + "code" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "procurement_rfq_details_updated_by_users_id_fk": { + "name": "procurement_rfq_details_updated_by_users_id_fk", + "tableFrom": "procurement_rfq_details", + "tableTo": "users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.procurement_rfqs": { + "name": "procurement_rfqs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "series": { + "name": "series", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "item_name": { + "name": "item_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "rfq_send_date": { + "name": "rfq_send_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'RFQ Created'" + }, + "rfq_sealed_yn": { + "name": "rfq_sealed_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "pic_code": { + "name": "pic_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_by": { + "name": "sent_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "procurement_rfqs_project_id_projects_id_fk": { + "name": "procurement_rfqs_project_id_projects_id_fk", + "tableFrom": "procurement_rfqs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "procurement_rfqs_sent_by_users_id_fk": { + "name": "procurement_rfqs_sent_by_users_id_fk", + "tableFrom": "procurement_rfqs", + "tableTo": "users", + "columnsFrom": [ + "sent_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "procurement_rfqs_created_by_users_id_fk": { + "name": "procurement_rfqs_created_by_users_id_fk", + "tableFrom": "procurement_rfqs", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "procurement_rfqs_updated_by_users_id_fk": { + "name": "procurement_rfqs_updated_by_users_id_fk", + "tableFrom": "procurement_rfqs", + "tableTo": "users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "procurement_rfqs_rfq_code_unique": { + "name": "procurement_rfqs_rfq_code_unique", + "nullsNotDistinct": false, + "columns": [ + "rfq_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.procurement_vendor_quotations": { + "name": "procurement_vendor_quotations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quotation_code": { + "name": "quotation_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "quotation_version": { + "name": "quotation_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "total_items_count": { + "name": "total_items_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sub_total": { + "name": "sub_total", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "tax_total": { + "name": "tax_total", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "discount_total": { + "name": "discount_total", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_price": { + "name": "total_price", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "currency": { + "name": "currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'USD'" + }, + "valid_until": { + "name": "valid_until", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "estimated_delivery_date": { + "name": "estimated_delivery_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "payment_terms_code": { + "name": "payment_terms_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "incoterms_code": { + "name": "incoterms_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "incoterms_detail": { + "name": "incoterms_detail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'Draft'" + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "procurement_vendor_quotations_rfq_id_procurement_rfqs_id_fk": { + "name": "procurement_vendor_quotations_rfq_id_procurement_rfqs_id_fk", + "tableFrom": "procurement_vendor_quotations", + "tableTo": "procurement_rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "procurement_vendor_quotations_vendor_id_vendors_id_fk": { + "name": "procurement_vendor_quotations_vendor_id_vendors_id_fk", + "tableFrom": "procurement_vendor_quotations", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "procurement_vendor_quotations_payment_terms_code_payment_terms_code_fk": { + "name": "procurement_vendor_quotations_payment_terms_code_payment_terms_code_fk", + "tableFrom": "procurement_vendor_quotations", + "tableTo": "payment_terms", + "columnsFrom": [ + "payment_terms_code" + ], + "columnsTo": [ + "code" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "procurement_vendor_quotations_incoterms_code_incoterms_code_fk": { + "name": "procurement_vendor_quotations_incoterms_code_incoterms_code_fk", + "tableFrom": "procurement_vendor_quotations", + "tableTo": "incoterms", + "columnsFrom": [ + "incoterms_code" + ], + "columnsTo": [ + "code" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preset_shares": { + "name": "preset_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "preset_id": { + "name": "preset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "shared_with_user_id": { + "name": "shared_with_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "preset_shares_preset_id_table_presets_id_fk": { + "name": "preset_shares_preset_id_table_presets_id_fk", + "tableFrom": "preset_shares", + "tableTo": "table_presets", + "columnsFrom": [ + "preset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_presets": { + "name": "table_presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_shared": { + "name": "is_shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tech_sales_attachments": { + "name": "tech_sales_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "attachment_type": { + "name": "attachment_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "tech_sales_rfq_id": { + "name": "tech_sales_rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "original_file_name": { + "name": "original_file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tech_sales_attachments_tech_sales_rfq_id_tech_sales_rfqs_id_fk": { + "name": "tech_sales_attachments_tech_sales_rfq_id_tech_sales_rfqs_id_fk", + "tableFrom": "tech_sales_attachments", + "tableTo": "tech_sales_rfqs", + "columnsFrom": [ + "tech_sales_rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tech_sales_attachments_created_by_users_id_fk": { + "name": "tech_sales_attachments_created_by_users_id_fk", + "tableFrom": "tech_sales_attachments", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tech_sales_rfq_comment_attachments": { + "name": "tech_sales_rfq_comment_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "quotation_id": { + "name": "quotation_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "file_path": { + "name": "file_path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "is_vendor_upload": { + "name": "is_vendor_upload", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tech_sales_rfq_comment_attachments_rfq_id_tech_sales_rfqs_id_fk": { + "name": "tech_sales_rfq_comment_attachments_rfq_id_tech_sales_rfqs_id_fk", + "tableFrom": "tech_sales_rfq_comment_attachments", + "tableTo": "tech_sales_rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tech_sales_rfq_comment_attachments_comment_id_tech_sales_rfq_comments_id_fk": { + "name": "tech_sales_rfq_comment_attachments_comment_id_tech_sales_rfq_comments_id_fk", + "tableFrom": "tech_sales_rfq_comment_attachments", + "tableTo": "tech_sales_rfq_comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tech_sales_rfq_comment_attachments_quotation_id_tech_sales_vendor_quotations_id_fk": { + "name": "tech_sales_rfq_comment_attachments_quotation_id_tech_sales_vendor_quotations_id_fk", + "tableFrom": "tech_sales_rfq_comment_attachments", + "tableTo": "tech_sales_vendor_quotations", + "columnsFrom": [ + "quotation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tech_sales_rfq_comment_attachments_uploaded_by_users_id_fk": { + "name": "tech_sales_rfq_comment_attachments_uploaded_by_users_id_fk", + "tableFrom": "tech_sales_rfq_comment_attachments", + "tableTo": "users", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tech_sales_rfq_comment_attachments_vendor_id_tech_vendors_id_fk": { + "name": "tech_sales_rfq_comment_attachments_vendor_id_tech_vendors_id_fk", + "tableFrom": "tech_sales_rfq_comment_attachments", + "tableTo": "tech_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tech_sales_rfq_comments": { + "name": "tech_sales_rfq_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_vendor_comment": { + "name": "is_vendor_comment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_read": { + "name": "is_read", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tech_sales_rfq_comments_rfq_id_tech_sales_rfqs_id_fk": { + "name": "tech_sales_rfq_comments_rfq_id_tech_sales_rfqs_id_fk", + "tableFrom": "tech_sales_rfq_comments", + "tableTo": "tech_sales_rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tech_sales_rfq_comments_vendor_id_tech_vendors_id_fk": { + "name": "tech_sales_rfq_comments_vendor_id_tech_vendors_id_fk", + "tableFrom": "tech_sales_rfq_comments", + "tableTo": "tech_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tech_sales_rfq_comments_user_id_users_id_fk": { + "name": "tech_sales_rfq_comments_user_id_users_id_fk", + "tableFrom": "tech_sales_rfq_comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tech_sales_rfq_comments_parent_comment_id_tech_sales_rfq_comments_id_fk": { + "name": "tech_sales_rfq_comments_parent_comment_id_tech_sales_rfq_comments_id_fk", + "tableFrom": "tech_sales_rfq_comments", + "tableTo": "tech_sales_rfq_comments", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tech_sales_rfq_items": { + "name": "tech_sales_rfq_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_shipbuilding_id": { + "name": "item_shipbuilding_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "item_offshore_top_id": { + "name": "item_offshore_top_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "item_offshore_hull_id": { + "name": "item_offshore_hull_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "item_type": { + "name": "item_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tech_sales_rfq_items_rfq_id_tech_sales_rfqs_id_fk": { + "name": "tech_sales_rfq_items_rfq_id_tech_sales_rfqs_id_fk", + "tableFrom": "tech_sales_rfq_items", + "tableTo": "tech_sales_rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tech_sales_rfq_items_item_shipbuilding_id_item_shipbuilding_id_fk": { + "name": "tech_sales_rfq_items_item_shipbuilding_id_item_shipbuilding_id_fk", + "tableFrom": "tech_sales_rfq_items", + "tableTo": "item_shipbuilding", + "columnsFrom": [ + "item_shipbuilding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tech_sales_rfq_items_item_offshore_top_id_item_offshore_top_id_fk": { + "name": "tech_sales_rfq_items_item_offshore_top_id_item_offshore_top_id_fk", + "tableFrom": "tech_sales_rfq_items", + "tableTo": "item_offshore_top", + "columnsFrom": [ + "item_offshore_top_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tech_sales_rfq_items_item_offshore_hull_id_item_offshore_hull_id_fk": { + "name": "tech_sales_rfq_items_item_offshore_hull_id_item_offshore_hull_id_fk", + "tableFrom": "tech_sales_rfq_items", + "tableTo": "item_offshore_hull", + "columnsFrom": [ + "item_offshore_hull_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tech_sales_rfqs": { + "name": "tech_sales_rfqs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "bidding_project_id": { + "name": "bidding_project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "material_code": { + "name": "material_code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "rfq_send_date": { + "name": "rfq_send_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'RFQ Created'" + }, + "pic_code": { + "name": "pic_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "sent_by": { + "name": "sent_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "cancel_reason": { + "name": "cancel_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rfq_type": { + "name": "rfq_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'SHIP'" + } + }, + "indexes": {}, + "foreignKeys": { + "tech_sales_rfqs_bidding_project_id_bidding_projects_id_fk": { + "name": "tech_sales_rfqs_bidding_project_id_bidding_projects_id_fk", + "tableFrom": "tech_sales_rfqs", + "tableTo": "bidding_projects", + "columnsFrom": [ + "bidding_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tech_sales_rfqs_sent_by_users_id_fk": { + "name": "tech_sales_rfqs_sent_by_users_id_fk", + "tableFrom": "tech_sales_rfqs", + "tableTo": "users", + "columnsFrom": [ + "sent_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tech_sales_rfqs_created_by_users_id_fk": { + "name": "tech_sales_rfqs_created_by_users_id_fk", + "tableFrom": "tech_sales_rfqs", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tech_sales_rfqs_updated_by_users_id_fk": { + "name": "tech_sales_rfqs_updated_by_users_id_fk", + "tableFrom": "tech_sales_rfqs", + "tableTo": "users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tech_sales_rfqs_rfq_code_unique": { + "name": "tech_sales_rfqs_rfq_code_unique", + "nullsNotDistinct": false, + "columns": [ + "rfq_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tech_sales_vendor_quotations": { + "name": "tech_sales_vendor_quotations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quotation_code": { + "name": "quotation_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "quotation_version": { + "name": "quotation_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "total_price": { + "name": "total_price", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "valid_until": { + "name": "valid_until", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'Draft'" + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tech_sales_vendor_quotations_rfq_id_tech_sales_rfqs_id_fk": { + "name": "tech_sales_vendor_quotations_rfq_id_tech_sales_rfqs_id_fk", + "tableFrom": "tech_sales_vendor_quotations", + "tableTo": "tech_sales_rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tech_sales_vendor_quotations_vendor_id_tech_vendors_id_fk": { + "name": "tech_sales_vendor_quotations_vendor_id_tech_vendors_id_fk", + "tableFrom": "tech_sales_vendor_quotations", + "tableTo": "tech_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ocr_rotation_attempts": { + "name": "ocr_rotation_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rotation": { + "name": "rotation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false + }, + "tables_found": { + "name": "tables_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "text_quality": { + "name": "text_quality", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false + }, + "keyword_count": { + "name": "keyword_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "score": { + "name": "score", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false + }, + "extracted_rows_count": { + "name": "extracted_rows_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ocr_rotation_attempts_session_id_ocr_sessions_id_fk": { + "name": "ocr_rotation_attempts_session_id_ocr_sessions_id_fk", + "tableFrom": "ocr_rotation_attempts", + "tableTo": "ocr_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ocr_rows": { + "name": "ocr_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "table_id": { + "name": "table_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "row_index": { + "name": "row_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "report_no": { + "name": "report_no", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "no": { + "name": "no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "identification_no": { + "name": "identification_no", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "tag_no": { + "name": "tag_no", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "joint_no": { + "name": "joint_no", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "joint_type": { + "name": "joint_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "welding_date": { + "name": "welding_date", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false + }, + "source_table": { + "name": "source_table", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_row": { + "name": "source_row", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ocr_report_no_unique": { + "name": "idx_ocr_report_no_unique", + "columns": [ + { + "expression": "report_no", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "no", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_no", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "joint_no", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "joint_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ocr_rows_table_id_ocr_tables_id_fk": { + "name": "ocr_rows_table_id_ocr_tables_id_fk", + "tableFrom": "ocr_rows", + "tableTo": "ocr_tables", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ocr_rows_session_id_ocr_sessions_id_fk": { + "name": "ocr_rows_session_id_ocr_sessions_id_fk", + "tableFrom": "ocr_rows", + "tableTo": "ocr_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ocr_rows_user_id_users_id_fk": { + "name": "ocr_rows_user_id_users_id_fk", + "tableFrom": "ocr_rows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ocr_sessions": { + "name": "ocr_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "processing_time": { + "name": "processing_time", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "best_rotation": { + "name": "best_rotation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tables": { + "name": "total_tables", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_rows": { + "name": "total_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "image_enhanced": { + "name": "image_enhanced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pdf_converted": { + "name": "pdf_converted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "warnings": { + "name": "warnings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ocr_tables": { + "name": "ocr_tables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "table_index": { + "name": "table_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ocr_tables_session_id_ocr_sessions_id_fk": { + "name": "ocr_tables_session_id_ocr_sessions_id_fk", + "tableFrom": "ocr_tables", + "tableTo": "ocr_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.b_rfq_attachment_revisions": { + "name": "b_rfq_attachment_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "attachment_id": { + "name": "attachment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "revision_no": { + "name": "revision_no", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "revision_comment": { + "name": "revision_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_latest": { + "name": "is_latest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "original_file_name": { + "name": "original_file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "latest_revision_idx": { + "name": "latest_revision_idx", + "columns": [ + { + "expression": "attachment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"b_rfq_attachment_revisions\".\"is_latest\" = $1", + "concurrently": false, + "method": "btree", + "with": {} + }, + "attachment_revision_idx": { + "name": "attachment_revision_idx", + "columns": [ + { + "expression": "attachment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_no", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "b_rfq_attachment_revisions_attachment_id_b_rfq_attachments_id_fk": { + "name": "b_rfq_attachment_revisions_attachment_id_b_rfq_attachments_id_fk", + "tableFrom": "b_rfq_attachment_revisions", + "tableTo": "b_rfq_attachments", + "columnsFrom": [ + "attachment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "b_rfq_attachment_revisions_created_by_users_id_fk": { + "name": "b_rfq_attachment_revisions_created_by_users_id_fk", + "tableFrom": "b_rfq_attachment_revisions", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.b_rfqs": { + "name": "b_rfqs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "pic_code": { + "name": "pic_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "pic_name": { + "name": "pic_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "eng_pic_name": { + "name": "eng_pic_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "project_company": { + "name": "project_company", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_flag": { + "name": "project_flag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_site": { + "name": "project_site", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "package_no": { + "name": "package_no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "package_name": { + "name": "package_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "b_rfqs_project_id_projects_id_fk": { + "name": "b_rfqs_project_id_projects_id_fk", + "tableFrom": "b_rfqs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "b_rfqs_created_by_users_id_fk": { + "name": "b_rfqs_created_by_users_id_fk", + "tableFrom": "b_rfqs", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "b_rfqs_updated_by_users_id_fk": { + "name": "b_rfqs_updated_by_users_id_fk", + "tableFrom": "b_rfqs", + "tableTo": "users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "b_rfqs_rfq_code_unique": { + "name": "b_rfqs_rfq_code_unique", + "nullsNotDistinct": false, + "columns": [ + "rfq_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.b_rfq_attachments": { + "name": "b_rfq_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "attachment_type": { + "name": "attachment_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "serial_no": { + "name": "serial_no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "current_revision": { + "name": "current_revision", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'Rev.0'" + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "b_rfq_attachments_rfq_id_b_rfqs_id_fk": { + "name": "b_rfq_attachments_rfq_id_b_rfqs_id_fk", + "tableFrom": "b_rfq_attachments", + "tableTo": "b_rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "b_rfq_attachments_created_by_users_id_fk": { + "name": "b_rfq_attachments_created_by_users_id_fk", + "tableFrom": "b_rfq_attachments", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.final_rfq": { + "name": "final_rfq", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "final_rfq_status": { + "name": "final_rfq_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "valid_date": { + "name": "valid_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "incoterms_code": { + "name": "incoterms_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "gtc": { + "name": "gtc", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "gtc_valid_date": { + "name": "gtc_valid_date", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "classification": { + "name": "classification", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "sparepart": { + "name": "sparepart", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "short_list": { + "name": "short_list", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "return_yn": { + "name": "return_yn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cp_request_yn": { + "name": "cp_request_yn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "prject_gtc_yn": { + "name": "prject_gtc_yn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "return_revision": { + "name": "return_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "currency": { + "name": "currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'KRW'" + }, + "payment_terms_code": { + "name": "payment_terms_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "tax_code": { + "name": "tax_code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "default": "'VV'" + }, + "delivery_date": { + "name": "delivery_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "place_of_shipping": { + "name": "place_of_shipping", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "place_of_destination": { + "name": "place_of_destination", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "firsttime_yn": { + "name": "firsttime_yn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "material_price_related_yn": { + "name": "material_price_related_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor_remark": { + "name": "vendor_remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "final_rfq_rfq_id_b_rfqs_id_fk": { + "name": "final_rfq_rfq_id_b_rfqs_id_fk", + "tableFrom": "final_rfq", + "tableTo": "b_rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "final_rfq_vendor_id_vendors_id_fk": { + "name": "final_rfq_vendor_id_vendors_id_fk", + "tableFrom": "final_rfq", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "final_rfq_incoterms_code_incoterms_code_fk": { + "name": "final_rfq_incoterms_code_incoterms_code_fk", + "tableFrom": "final_rfq", + "tableTo": "incoterms", + "columnsFrom": [ + "incoterms_code" + ], + "columnsTo": [ + "code" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "final_rfq_payment_terms_code_payment_terms_code_fk": { + "name": "final_rfq_payment_terms_code_payment_terms_code_fk", + "tableFrom": "final_rfq", + "tableTo": "payment_terms", + "columnsFrom": [ + "payment_terms_code" + ], + "columnsTo": [ + "code" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.initial_rfq": { + "name": "initial_rfq", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "initial_rfq_status": { + "name": "initial_rfq_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "valid_date": { + "name": "valid_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "incoterms_code": { + "name": "incoterms_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "gtc": { + "name": "gtc", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "gtc_valid_date": { + "name": "gtc_valid_date", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "classification": { + "name": "classification", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "sparepart": { + "name": "sparepart", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "short_list": { + "name": "short_list", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "return_yn": { + "name": "return_yn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cp_request_yn": { + "name": "cp_request_yn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "prject_gtc_yn": { + "name": "prject_gtc_yn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "return_revision": { + "name": "return_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rfq_revision": { + "name": "rfq_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "initial_rfq_rfq_id_b_rfqs_id_fk": { + "name": "initial_rfq_rfq_id_b_rfqs_id_fk", + "tableFrom": "initial_rfq", + "tableTo": "b_rfqs", + "columnsFrom": [ + "rfq_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "initial_rfq_vendor_id_vendors_id_fk": { + "name": "initial_rfq_vendor_id_vendors_id_fk", + "tableFrom": "initial_rfq", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "initial_rfq_incoterms_code_incoterms_code_fk": { + "name": "initial_rfq_incoterms_code_incoterms_code_fk", + "tableFrom": "initial_rfq", + "tableTo": "incoterms", + "columnsFrom": [ + "incoterms_code" + ], + "columnsTo": [ + "code" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_attachment_responses": { + "name": "vendor_attachment_responses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "attachment_id": { + "name": "attachment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rfq_type": { + "name": "rfq_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "rfq_record_id": { + "name": "rfq_record_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "response_status": { + "name": "response_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'NOT_RESPONDED'" + }, + "current_revision": { + "name": "current_revision", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'Rev.0'" + }, + "responded_revision": { + "name": "responded_revision", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "response_comment": { + "name": "response_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor_comment": { + "name": "vendor_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision_request_comment": { + "name": "revision_request_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "responded_at": { + "name": "responded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revision_requested_at": { + "name": "revision_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "vendor_response_idx": { + "name": "vendor_response_idx", + "columns": [ + { + "expression": "attachment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rfq_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vendor_attachment_responses_attachment_id_b_rfq_attachments_id_fk": { + "name": "vendor_attachment_responses_attachment_id_b_rfq_attachments_id_fk", + "tableFrom": "vendor_attachment_responses", + "tableTo": "b_rfq_attachments", + "columnsFrom": [ + "attachment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vendor_attachment_responses_vendor_id_vendors_id_fk": { + "name": "vendor_attachment_responses_vendor_id_vendors_id_fk", + "tableFrom": "vendor_attachment_responses", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_response_attachments_b": { + "name": "vendor_response_attachments_b", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_response_id": { + "name": "vendor_response_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "original_file_name": { + "name": "original_file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_response_attachments_b_vendor_response_id_vendor_attachment_responses_id_fk": { + "name": "vendor_response_attachments_b_vendor_response_id_vendor_attachment_responses_id_fk", + "tableFrom": "vendor_response_attachments_b", + "tableTo": "vendor_attachment_responses", + "columnsFrom": [ + "vendor_response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vendor_response_attachments_b_uploaded_by_users_id_fk": { + "name": "vendor_response_attachments_b_uploaded_by_users_id_fk", + "tableFrom": "vendor_response_attachments_b", + "tableTo": "users", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_response_history": { + "name": "vendor_response_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_response_id": { + "name": "vendor_response_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "previous_status": { + "name": "previous_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "new_status": { + "name": "new_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_by": { + "name": "action_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "action_at": { + "name": "action_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_response_history_vendor_response_id_vendor_attachment_responses_id_fk": { + "name": "vendor_response_history_vendor_response_id_vendor_attachment_responses_id_fk", + "tableFrom": "vendor_response_history", + "tableTo": "vendor_attachment_responses", + "columnsFrom": [ + "vendor_response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vendor_response_history_action_by_users_id_fk": { + "name": "vendor_response_history_action_by_users_id_fk", + "tableFrom": "vendor_response_history", + "tableTo": "users", + "columnsFrom": [ + "action_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tech_vendor_attachments": { + "name": "tech_vendor_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "attachment_type": { + "name": "attachment_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'GENERAL'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tech_vendor_attachments_vendor_id_tech_vendors_id_fk": { + "name": "tech_vendor_attachments_vendor_id_tech_vendors_id_fk", + "tableFrom": "tech_vendor_attachments", + "tableTo": "tech_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tech_vendor_candidates": { + "name": "tech_vendor_candidates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "company_name": { + "name": "company_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "contact_email": { + "name": "contact_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "contact_phone": { + "name": "contact_phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'COLLECTED'" + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "items": { + "name": "items", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tech_vendor_candidates_vendor_id_tech_vendors_id_fk": { + "name": "tech_vendor_candidates_vendor_id_tech_vendors_id_fk", + "tableFrom": "tech_vendor_candidates", + "tableTo": "tech_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tech_vendor_contacts": { + "name": "tech_vendor_contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "contact_name": { + "name": "contact_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "contact_position": { + "name": "contact_position", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "contact_email": { + "name": "contact_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "contact_phone": { + "name": "contact_phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tech_vendor_contacts_vendor_id_tech_vendors_id_fk": { + "name": "tech_vendor_contacts_vendor_id_tech_vendors_id_fk", + "tableFrom": "tech_vendor_contacts", + "tableTo": "tech_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tech_vendor_possible_items": { + "name": "tech_vendor_possible_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "item_name": { + "name": "item_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tech_vendor_possible_items_vendor_id_tech_vendors_id_fk": { + "name": "tech_vendor_possible_items_vendor_id_tech_vendors_id_fk", + "tableFrom": "tech_vendor_possible_items", + "tableTo": "tech_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tech_vendor_possible_items_item_code_items_item_code_fk": { + "name": "tech_vendor_possible_items_item_code_items_item_code_fk", + "tableFrom": "tech_vendor_possible_items", + "tableTo": "items", + "columnsFrom": [ + "item_code" + ], + "columnsTo": [ + "item_code" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tech_vendors": { + "name": "tech_vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country_eng": { + "name": "country_eng", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country_fab": { + "name": "country_fab", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "agent_phone": { + "name": "agent_phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "agent_email": { + "name": "agent_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tech_vendor_type": { + "name": "tech_vendor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "representative_name": { + "name": "representative_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "representative_email": { + "name": "representative_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "representative_phone": { + "name": "representative_phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "representative_birth": { + "name": "representative_birth", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "items": { + "name": "items", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.esg_answer_options": { + "name": "esg_answer_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "esg_evaluation_item_id": { + "name": "esg_evaluation_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "answer_text": { + "name": "answer_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true + }, + "order_index": { + "name": "order_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "esg_answer_options_esg_evaluation_item_id_esg_evaluation_items_id_fk": { + "name": "esg_answer_options_esg_evaluation_item_id_esg_evaluation_items_id_fk", + "tableFrom": "esg_answer_options", + "tableTo": "esg_evaluation_items", + "columnsFrom": [ + "esg_evaluation_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.esg_evaluation_items": { + "name": "esg_evaluation_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "esg_evaluation_id": { + "name": "esg_evaluation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "evaluation_item": { + "name": "evaluation_item", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evaluation_item_description": { + "name": "evaluation_item_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order_index": { + "name": "order_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "esg_evaluation_items_esg_evaluation_id_esg_evaluations_id_fk": { + "name": "esg_evaluation_items_esg_evaluation_id_esg_evaluations_id_fk", + "tableFrom": "esg_evaluation_items", + "tableTo": "esg_evaluations", + "columnsFrom": [ + "esg_evaluation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.esg_evaluation_responses": { + "name": "esg_evaluation_responses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "submission_id": { + "name": "submission_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "esg_evaluation_item_id": { + "name": "esg_evaluation_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "esg_answer_option_id": { + "name": "esg_answer_option_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "selected_score": { + "name": "selected_score", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true + }, + "additional_comments": { + "name": "additional_comments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "esg_evaluation_responses_submission_id_evaluation_submissions_id_fk": { + "name": "esg_evaluation_responses_submission_id_evaluation_submissions_id_fk", + "tableFrom": "esg_evaluation_responses", + "tableTo": "evaluation_submissions", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "esg_evaluation_responses_esg_evaluation_item_id_esg_evaluation_items_id_fk": { + "name": "esg_evaluation_responses_esg_evaluation_item_id_esg_evaluation_items_id_fk", + "tableFrom": "esg_evaluation_responses", + "tableTo": "esg_evaluation_items", + "columnsFrom": [ + "esg_evaluation_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "esg_evaluation_responses_esg_answer_option_id_esg_answer_options_id_fk": { + "name": "esg_evaluation_responses_esg_answer_option_id_esg_answer_options_id_fk", + "tableFrom": "esg_evaluation_responses", + "tableTo": "esg_answer_options", + "columnsFrom": [ + "esg_answer_option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.esg_evaluations": { + "name": "esg_evaluations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "serial_number": { + "name": "serial_number", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "inspection_item": { + "name": "inspection_item", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "esg_evaluations_serial_number_unique": { + "name": "esg_evaluations_serial_number_unique", + "nullsNotDistinct": false, + "columns": [ + "serial_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.evaluation_submissions": { + "name": "evaluation_submissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "submission_id": { + "name": "submission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "evaluation_year": { + "name": "evaluation_year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "evaluation_round": { + "name": "evaluation_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "submission_status": { + "name": "submission_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "review_comments": { + "name": "review_comments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "average_esg_score": { + "name": "average_esg_score", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "total_general_items": { + "name": "total_general_items", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "completed_general_items": { + "name": "completed_general_items", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "total_esg_items": { + "name": "total_esg_items", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "completed_esg_items": { + "name": "completed_esg_items", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "evaluation_submissions_company_id_vendors_id_fk": { + "name": "evaluation_submissions_company_id_vendors_id_fk", + "tableFrom": "evaluation_submissions", + "tableTo": "vendors", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "evaluation_submissions_submission_id_unique": { + "name": "evaluation_submissions_submission_id_unique", + "nullsNotDistinct": false, + "columns": [ + "submission_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.general_evaluation_responses": { + "name": "general_evaluation_responses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "submission_id": { + "name": "submission_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "general_evaluation_id": { + "name": "general_evaluation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "response_text": { + "name": "response_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "review_comments": { + "name": "review_comments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "general_evaluation_responses_submission_id_evaluation_submissions_id_fk": { + "name": "general_evaluation_responses_submission_id_evaluation_submissions_id_fk", + "tableFrom": "general_evaluation_responses", + "tableTo": "evaluation_submissions", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "general_evaluation_responses_general_evaluation_id_general_evaluations_id_fk": { + "name": "general_evaluation_responses_general_evaluation_id_general_evaluations_id_fk", + "tableFrom": "general_evaluation_responses", + "tableTo": "general_evaluations", + "columnsFrom": [ + "general_evaluation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.general_evaluations": { + "name": "general_evaluations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "serial_number": { + "name": "serial_number", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "inspection_item": { + "name": "inspection_item", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remarks": { + "name": "remarks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "general_evaluations_serial_number_unique": { + "name": "general_evaluations_serial_number_unique", + "nullsNotDistinct": false, + "columns": [ + "serial_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendor_evaluation_attachments": { + "name": "vendor_evaluation_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "submission_id": { + "name": "submission_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "general_evaluation_response_id": { + "name": "general_evaluation_response_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "original_file_name": { + "name": "original_file_name", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "stored_file_name": { + "name": "stored_file_name", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_evaluation_attachments_submission_id_evaluation_submissions_id_fk": { + "name": "vendor_evaluation_attachments_submission_id_evaluation_submissions_id_fk", + "tableFrom": "vendor_evaluation_attachments", + "tableTo": "evaluation_submissions", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vendor_evaluation_attachments_general_evaluation_response_id_general_evaluation_responses_id_fk": { + "name": "vendor_evaluation_attachments_general_evaluation_response_id_general_evaluation_responses_id_fk", + "tableFrom": "vendor_evaluation_attachments", + "tableTo": "general_evaluation_responses", + "columnsFrom": [ + "general_evaluation_response_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vendor_evaluation_attachments_file_id_unique": { + "name": "vendor_evaluation_attachments_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.evaluation_target_reviewers": { + "name": "evaluation_target_reviewers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "evaluation_target_id": { + "name": "evaluation_target_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "department_code": { + "name": "department_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "department_name_from": { + "name": "department_name_from", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "reviewer_user_id": { + "name": "reviewer_user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "assigned_by": { + "name": "assigned_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "evaluation_target_reviewers_evaluation_target_id_evaluation_targets_id_fk": { + "name": "evaluation_target_reviewers_evaluation_target_id_evaluation_targets_id_fk", + "tableFrom": "evaluation_target_reviewers", + "tableTo": "evaluation_targets", + "columnsFrom": [ + "evaluation_target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "evaluation_target_reviewers_reviewer_user_id_users_id_fk": { + "name": "evaluation_target_reviewers_reviewer_user_id_users_id_fk", + "tableFrom": "evaluation_target_reviewers", + "tableTo": "users", + "columnsFrom": [ + "reviewer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evaluation_target_reviewers_assigned_by_users_id_fk": { + "name": "evaluation_target_reviewers_assigned_by_users_id_fk", + "tableFrom": "evaluation_target_reviewers", + "tableTo": "users", + "columnsFrom": [ + "assigned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_target_department": { + "name": "unique_target_department", + "nullsNotDistinct": false, + "columns": [ + "evaluation_target_id", + "department_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.evaluation_target_reviews": { + "name": "evaluation_target_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "evaluation_target_id": { + "name": "evaluation_target_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reviewer_user_id": { + "name": "reviewer_user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "department_code": { + "name": "department_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "is_approved": { + "name": "is_approved", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "review_comment": { + "name": "review_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "evaluation_target_reviews_evaluation_target_id_evaluation_targets_id_fk": { + "name": "evaluation_target_reviews_evaluation_target_id_evaluation_targets_id_fk", + "tableFrom": "evaluation_target_reviews", + "tableTo": "evaluation_targets", + "columnsFrom": [ + "evaluation_target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "evaluation_target_reviews_reviewer_user_id_users_id_fk": { + "name": "evaluation_target_reviews_reviewer_user_id_users_id_fk", + "tableFrom": "evaluation_target_reviews", + "tableTo": "users", + "columnsFrom": [ + "reviewer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_target_reviewer": { + "name": "unique_target_reviewer", + "nullsNotDistinct": false, + "columns": [ + "evaluation_target_id", + "reviewer_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.evaluation_targets": { + "name": "evaluation_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "evaluation_year": { + "name": "evaluation_year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "division": { + "name": "division", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "domestic_foreign": { + "name": "domestic_foreign", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "material_type": { + "name": "material_type", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "admin_comment": { + "name": "admin_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_user_id": { + "name": "admin_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "consolidated_comment": { + "name": "consolidated_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consensus_status": { + "name": "consensus_status", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "confirmed_by": { + "name": "confirmed_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ld_claim_count": { + "name": "ld_claim_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "ld_claim_amount": { + "name": "ld_claim_amount", + "type": "numeric(15, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "ld_claim_currency": { + "name": "ld_claim_currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'KRW'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "evaluation_targets_vendor_id_vendors_id_fk": { + "name": "evaluation_targets_vendor_id_vendors_id_fk", + "tableFrom": "evaluation_targets", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evaluation_targets_admin_user_id_users_id_fk": { + "name": "evaluation_targets_admin_user_id_users_id_fk", + "tableFrom": "evaluation_targets", + "tableTo": "users", + "columnsFrom": [ + "admin_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evaluation_targets_confirmed_by_users_id_fk": { + "name": "evaluation_targets_confirmed_by_users_id_fk", + "tableFrom": "evaluation_targets", + "tableTo": "users", + "columnsFrom": [ + "confirmed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_gtc_files": { + "name": "project_gtc_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "original_file_name": { + "name": "original_file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_gtc_files_project_id_projects_id_fk": { + "name": "project_gtc_files_project_id_projects_id_fk", + "tableFrom": "project_gtc_files", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.user_domain": { + "name": "user_domain", + "schema": "public", + "values": [ + "evcp", + "partners" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.contracts_detail_view": { + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "contracts_detail_view_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "contract_no": { + "name": "contract_no", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "contract_name": { + "name": "contract_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "payment_terms": { + "name": "payment_terms", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_terms": { + "name": "delivery_terms", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "delivery_location": { + "name": "delivery_location", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'KRW'" + }, + "total_amount": { + "name": "total_amount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "discount": { + "name": "discount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "tax": { + "name": "tax", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "shipping_fee": { + "name": "shipping_fee", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "net_total": { + "name": "net_total", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "partial_shipping_allowed": { + "name": "partial_shipping_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "partial_payment_allowed": { + "name": "partial_payment_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "remarks": { + "name": "remarks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "definition": "select \"contracts\".\"id\", \"contracts\".\"contract_no\", \"contracts\".\"contract_name\", \"contracts\".\"status\", \"contracts\".\"start_date\", \"contracts\".\"end_date\", \"contracts\".\"project_id\", \"projects\".\"code\", \"projects\".\"name\", \"contracts\".\"vendor_id\", \"vendors\".\"vendor_name\", \"contracts\".\"payment_terms\", \"contracts\".\"delivery_terms\", \"contracts\".\"delivery_date\", \"contracts\".\"delivery_location\", \"contracts\".\"currency\", \"contracts\".\"total_amount\", \"contracts\".\"discount\", \"contracts\".\"tax\", \"contracts\".\"shipping_fee\", \"contracts\".\"net_total\", \"contracts\".\"partial_shipping_allowed\", \"contracts\".\"partial_payment_allowed\", \"contracts\".\"remarks\", \"contracts\".\"version\", \"contracts\".\"created_at\", \"contracts\".\"updated_at\", EXISTS (\n SELECT 1 \n FROM \"contract_envelopes\" \n WHERE \"contract_envelopes\".\"contract_id\" = \"contracts\".\"id\"\n ) as \"has_signature\", COALESCE((\n SELECT json_agg(\n json_build_object(\n 'id', ci.id,\n 'itemId', ci.item_id,\n 'description', ci.description,\n 'quantity', ci.quantity,\n 'unitPrice', ci.unit_price,\n 'taxRate', ci.tax_rate,\n 'taxAmount', ci.tax_amount,\n 'totalLineAmount', ci.total_line_amount,\n 'remark', ci.remark,\n 'createdAt', ci.created_at,\n 'updatedAt', ci.updated_at\n )\n )\n FROM \"contract_items\" AS ci\n WHERE ci.contract_id = \"contracts\".\"id\"\n ), '[]') as \"items\", COALESCE((\n SELECT json_agg(\n json_build_object(\n 'id', ce.id,\n 'envelopeId', ce.envelope_id,\n 'documentId', ce.document_id,\n 'envelopeStatus', ce.envelope_status,\n 'fileName', ce.file_name,\n 'filePath', ce.file_path,\n 'createdAt', ce.created_at,\n 'updatedAt', ce.updated_at,\n 'signers', (\n SELECT json_agg(\n json_build_object(\n 'id', cs.id,\n 'vendorContactId', cs.vendor_contact_id,\n 'signerType', cs.signer_type,\n 'signerEmail', cs.signer_email,\n 'signerName', cs.signer_name,\n 'signerPosition', cs.signer_position,\n 'signerStatus', cs.signer_status,\n 'signedAt', cs.signed_at\n )\n )\n FROM \"contract_signers\" AS cs\n WHERE cs.envelope_id = ce.id\n )\n )\n )\n FROM \"contract_envelopes\" AS ce\n WHERE ce.contract_id = \"contracts\".\"id\"\n ), '[]') as \"envelopes\" from \"contracts\" left join \"projects\" on \"contracts\".\"project_id\" = \"projects\".\"id\" left join \"vendors\" on \"contracts\".\"vendor_id\" = \"vendors\".\"id\"", + "name": "contracts_detail_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.poa_detail_view": { + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "poa_detail_view_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "contract_no": { + "name": "contract_no", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "change_reason": { + "name": "change_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approval_status": { + "name": "approval_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'PENDING'" + }, + "delivery_terms": { + "name": "delivery_terms", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "delivery_location": { + "name": "delivery_location", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "total_amount": { + "name": "total_amount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "discount": { + "name": "discount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "tax": { + "name": "tax", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "shipping_fee": { + "name": "shipping_fee", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "net_total": { + "name": "net_total", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "definition": "select \"poa\".\"id\", \"poa\".\"contract_no\", \"contracts\".\"project_id\", \"contracts\".\"vendor_id\", \"poa\".\"change_reason\", \"poa\".\"approval_status\", \"contracts\".\"contract_name\" as \"original_contract_name\", \"contracts\".\"status\" as \"original_status\", \"contracts\".\"start_date\" as \"original_start_date\", \"contracts\".\"end_date\" as \"original_end_date\", \"poa\".\"delivery_terms\", \"poa\".\"delivery_date\", \"poa\".\"delivery_location\", \"poa\".\"currency\", \"poa\".\"total_amount\", \"poa\".\"discount\", \"poa\".\"tax\", \"poa\".\"shipping_fee\", \"poa\".\"net_total\", \"poa\".\"created_at\", \"poa\".\"updated_at\", EXISTS (\n SELECT 1 \n FROM \"contract_envelopes\" \n WHERE \"contract_envelopes\".\"contract_id\" = \"poa\".\"id\"\n ) as \"has_signature\" from \"poa\" left join \"contracts\" on \"poa\".\"contract_no\" = \"contracts\".\"contract_no\"", + "name": "poa_detail_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.project_approved_vendors": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'PENDING_REVIEW'" + }, + "name_ko": { + "name": "name_ko", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name_en": { + "name": "name_en", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'ship'" + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"vendors\".\"id\", \"vendors\".\"vendor_name\", \"vendors\".\"vendor_code\", \"vendors\".\"tax_id\", \"vendors\".\"email\", \"vendors\".\"phone\", \"vendors\".\"status\", \"vendor_types\".\"name_ko\", \"vendor_types\".\"name_en\", \"projects\".\"code\", \"projects\".\"name\", \"projects\".\"type\", \"vendor_pq_submissions\".\"submitted_at\", \"vendor_pq_submissions\".\"approved_at\" from \"vendors\" inner join \"vendor_pq_submissions\" on \"vendor_pq_submissions\".\"vendor_id\" = \"vendors\".\"id\" inner join \"projects\" on \"vendor_pq_submissions\".\"project_id\" = \"projects\".\"id\" left join \"vendor_types\" on \"vendors\".\"vendor_type_id\" = \"vendor_types\".\"id\" where \"vendor_pq_submissions\".\"status\" = 'APPROVED'", + "name": "project_approved_vendors", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_investigations_view": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pq_submission_id": { + "name": "pq_submission_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "requester_id": { + "name": "requester_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "qm_manager_id": { + "name": "qm_manager_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "investigation_status": { + "name": "investigation_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'PLANNED'" + }, + "evaluation_type": { + "name": "evaluation_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "investigation_address": { + "name": "investigation_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "investigation_method": { + "name": "investigation_method", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "scheduled_start_at": { + "name": "scheduled_start_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scheduled_end_at": { + "name": "scheduled_end_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forecasted_at": { + "name": "forecasted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "evaluation_score": { + "name": "evaluation_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "evaluation_result": { + "name": "evaluation_result", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "investigation_notes": { + "name": "investigation_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"vendor_investigations\".\"id\", \"vendor_investigations\".\"vendor_id\", \"vendor_investigations\".\"pq_submission_id\", \"vendor_investigations\".\"requester_id\", \"vendor_investigations\".\"qm_manager_id\", \"vendor_investigations\".\"investigation_status\", \"vendor_investigations\".\"evaluation_type\", \"vendor_investigations\".\"investigation_address\", \"vendor_investigations\".\"investigation_method\", \"vendor_investigations\".\"scheduled_start_at\", \"vendor_investigations\".\"scheduled_end_at\", \"vendor_investigations\".\"forecasted_at\", \"vendor_investigations\".\"requested_at\", \"vendor_investigations\".\"confirmed_at\", \"vendor_investigations\".\"completed_at\", \"vendor_investigations\".\"evaluation_score\", \"vendor_investigations\".\"evaluation_result\", \"vendor_investigations\".\"investigation_notes\", \"vendor_investigations\".\"created_at\", \"vendor_investigations\".\"updated_at\", \"vendors\".\"vendor_name\", \"vendors\".\"vendor_code\", requester.name as \"requesterName\", requester.email as \"requesterEmail\", qm_manager.name as \"qmManagerName\", qm_manager.email as \"qmManagerEmail\", (\n CASE \n WHEN EXISTS (\n SELECT 1 FROM vendor_investigation_attachments via \n WHERE via.investigation_id = \"vendor_investigations\".\"id\"\n ) \n THEN true \n ELSE false \n END\n ) as \"hasAttachments\" from \"vendor_investigations\" left join \"vendors\" on \"vendor_investigations\".\"vendor_id\" = \"vendors\".\"id\" left join users AS requester on \"vendor_investigations\".\"requester_id\" = requester.id left join users AS qm_manager on \"vendor_investigations\".\"qm_manager_id\" = qm_manager.id", + "name": "vendor_investigations_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.cbe_view": { + "columns": {}, + "definition": "select \"cbe_evaluations\".\"id\" as \"cbe_id\", \"cbe_evaluations\".\"rfq_id\" as \"rfq_id\", \"cbe_evaluations\".\"vendor_id\" as \"vendor_id\", \"cbe_evaluations\".\"total_cost\" as \"total_cost\", \"cbe_evaluations\".\"currency\" as \"currency\", \"cbe_evaluations\".\"payment_terms\" as \"payment_terms\", \"cbe_evaluations\".\"incoterms\" as \"incoterms\", \"cbe_evaluations\".\"result\" as \"result\", \"cbe_evaluations\".\"notes\" as \"notes\", \"cbe_evaluations\".\"evaluated_by\" as \"evaluated_by\", \"cbe_evaluations\".\"evaluated_at\" as \"evaluated_at\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"description\" as \"rfq_description\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"users\".\"name\" as \"evaluator_name\", \"users\".\"email\" as \"evaluator_email\" from \"cbe_evaluations\" inner join \"rfqs\" on \"cbe_evaluations\".\"rfq_id\" = \"rfqs\".\"id\" inner join \"vendors\" on \"cbe_evaluations\".\"vendor_id\" = \"vendors\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"users\" on \"cbe_evaluations\".\"evaluated_by\" = \"users\".\"id\"", + "name": "cbe_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.rfqs_view": { + "columns": {}, + "definition": "select \"rfqs\".\"id\" as \"rfq_id\", \"rfqs\".\"status\" as \"status\", \"rfqs\".\"created_at\" as \"created_at\", \"rfqs\".\"updated_at\" as \"updated_at\", \"rfqs\".\"created_by\" as \"created_by\", \"rfqs\".\"rfq_type\" as \"rfq_type\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"description\" as \"description\", \"rfqs\".\"due_date\" as \"due_date\", \"rfqs\".\"parent_rfq_id\" as \"parent_rfq_id\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"users\".\"email\" as \"user_email\", \"users\".\"name\" as \"user_name\", (\n SELECT COUNT(*) \n FROM \"rfq_items\" \n WHERE \"rfq_items\".\"rfq_id\" = \"rfqs\".\"id\"\n ) as \"item_count\", (\n SELECT COUNT(*) \n FROM \"rfq_attachments\" \n WHERE \"rfq_attachments\".\"rfq_id\" = \"rfqs\".\"id\"\n ) as \"attachment_count\" from \"rfqs\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"users\" on \"rfqs\".\"created_by\" = \"users\".\"id\"", + "name": "rfqs_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_cbe_view": { + "columns": {}, + "definition": "select \"vendors\".\"id\" as \"vendor_id\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"address\" as \"address\", \"vendors\".\"country\" as \"country\", \"vendors\".\"email\" as \"email\", \"vendors\".\"website\" as \"website\", \"vendors\".\"status\" as \"vendor_status\", \"vendor_responses\".\"id\" as \"vendor_response_id\", \"vendor_responses\".\"rfq_id\" as \"rfq_id\", \"vendor_responses\".\"response_status\" as \"rfq_vendor_status\", \"vendor_responses\".\"updated_at\" as \"rfq_vendor_updated\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"rfq_type\" as \"rfq_type\", \"rfqs\".\"description\" as \"description\", \"rfqs\".\"due_date\" as \"due_date\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"cbe_evaluations\".\"id\" as \"cbe_id\", \"cbe_evaluations\".\"result\" as \"cbe_result\", \"cbe_evaluations\".\"notes\" as \"cbe_note\", \"cbe_evaluations\".\"updated_at\" as \"cbe_updated\", \"cbe_evaluations\".\"total_cost\" as \"total_cost\", \"cbe_evaluations\".\"currency\" as \"currency\", \"cbe_evaluations\".\"payment_terms\" as \"payment_terms\", \"cbe_evaluations\".\"incoterms\" as \"incoterms\", \"cbe_evaluations\".\"delivery_schedule\" as \"delivery_schedule\" from \"vendors\" left join \"vendor_responses\" on \"vendor_responses\".\"vendor_id\" = \"vendors\".\"id\" left join \"rfqs\" on \"vendor_responses\".\"rfq_id\" = \"rfqs\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"cbe_evaluations\" on (\"cbe_evaluations\".\"vendor_id\" = \"vendors\".\"id\" and \"cbe_evaluations\".\"rfq_id\" = \"vendor_responses\".\"rfq_id\")", + "name": "vendor_cbe_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_response_cbe_view": { + "columns": {}, + "definition": "select \"vendor_responses\".\"id\" as \"response_id\", \"vendor_responses\".\"rfq_id\" as \"rfq_id\", \"vendor_responses\".\"vendor_id\" as \"vendor_id\", \"vendor_responses\".\"response_status\" as \"response_status\", \"vendor_responses\".\"notes\" as \"response_notes\", \"vendor_responses\".\"responded_by\" as \"responded_by\", \"vendor_responses\".\"responded_at\" as \"responded_at\", \"vendor_responses\".\"updated_at\" as \"response_updated_at\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"description\" as \"rfq_description\", \"rfqs\".\"due_date\" as \"rfq_due_date\", \"rfqs\".\"status\" as \"rfq_status\", \"rfqs\".\"rfq_type\" as \"rfq_type\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"status\" as \"vendor_status\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"vendor_commercial_responses\".\"id\" as \"commercial_response_id\", \"vendor_commercial_responses\".\"response_status\" as \"commercial_response_status\", \"vendor_commercial_responses\".\"total_price\" as \"total_price\", \"vendor_commercial_responses\".\"currency\" as \"currency\", \"vendor_commercial_responses\".\"payment_terms\" as \"payment_terms\", \"vendor_commercial_responses\".\"incoterms\" as \"incoterms\", \"vendor_commercial_responses\".\"delivery_period\" as \"delivery_period\", \"vendor_commercial_responses\".\"warranty_period\" as \"warranty_period\", \"vendor_commercial_responses\".\"validity_period\" as \"validity_period\", \"vendor_commercial_responses\".\"price_breakdown\" as \"price_breakdown\", \"vendor_commercial_responses\".\"commercial_notes\" as \"commercial_notes\", \"vendor_commercial_responses\".\"created_at\" as \"commercial_created_at\", \"vendor_commercial_responses\".\"updated_at\" as \"commercial_updated_at\", (\n SELECT COUNT(*) \n FROM \"vendor_response_attachments\" \n WHERE \"vendor_response_attachments\".\"response_id\" = \"vendor_responses\".\"id\"\n ) as \"attachment_count\", (\n SELECT COUNT(*) \n FROM \"vendor_response_attachments\" \n WHERE \"vendor_response_attachments\".\"commercial_response_id\" = \"vendor_commercial_responses\".\"id\"\n ) as \"commercial_attachment_count\", (\n SELECT COUNT(*) \n FROM \"vendor_response_attachments\" \n WHERE \"vendor_response_attachments\".\"response_id\" = \"vendor_responses\".\"id\"\n AND \"vendor_response_attachments\".\"attachment_type\" = 'TECHNICAL_SPEC'\n ) as \"technical_attachment_count\", (\n SELECT MAX(\"uploaded_at\") \n FROM \"vendor_response_attachments\" \n WHERE \"vendor_response_attachments\".\"response_id\" = \"vendor_responses\".\"id\"\n ) as \"latest_attachment_date\" from \"vendor_responses\" inner join \"rfqs\" on \"vendor_responses\".\"rfq_id\" = \"rfqs\".\"id\" inner join \"vendors\" on \"vendor_responses\".\"vendor_id\" = \"vendors\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"vendor_commercial_responses\" on \"vendor_commercial_responses\".\"response_id\" = \"vendor_responses\".\"id\"", + "name": "vendor_response_cbe_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_responses_view": { + "columns": {}, + "definition": "select \"vendor_responses\".\"id\" as \"response_id\", \"vendor_responses\".\"rfq_id\" as \"rfq_id\", \"vendor_responses\".\"vendor_id\" as \"vendor_id\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"description\" as \"rfq_description\", \"rfqs\".\"due_date\" as \"rfq_due_date\", \"rfqs\".\"status\" as \"rfq_status\", \"rfqs\".\"rfq_type\" as \"rfq_type\", \"rfqs\".\"created_at\" as \"rfq_created_at\", \"rfqs\".\"updated_at\" as \"rfq_updated_at\", \"rfqs\".\"created_by\" as \"rfq_created_by\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendor_responses\".\"response_status\" as \"response_status\", \"vendor_responses\".\"responded_at\" as \"responded_at\", CASE WHEN \"vendor_technical_responses\".\"id\" IS NOT NULL THEN TRUE ELSE FALSE END as \"has_technical_response\", \"vendor_technical_responses\".\"id\" as \"technical_response_id\", CASE WHEN \"vendor_commercial_responses\".\"id\" IS NOT NULL THEN TRUE ELSE FALSE END as \"has_commercial_response\", \"vendor_commercial_responses\".\"id\" as \"commercial_response_id\", \"vendor_commercial_responses\".\"total_price\" as \"total_price\", \"vendor_commercial_responses\".\"currency\" as \"currency\", \"rfq_evaluations\".\"id\" as \"tbe_id\", \"rfq_evaluations\".\"result\" as \"tbe_result\", \"cbe_evaluations\".\"id\" as \"cbe_id\", \"cbe_evaluations\".\"result\" as \"cbe_result\", (\n SELECT COUNT(*) \n FROM \"vendor_response_attachments\" \n WHERE \"vendor_response_attachments\".\"response_id\" = \"vendor_responses\".\"id\"\n ) as \"attachment_count\" from \"vendor_responses\" inner join \"rfqs\" on \"vendor_responses\".\"rfq_id\" = \"rfqs\".\"id\" inner join \"vendors\" on \"vendor_responses\".\"vendor_id\" = \"vendors\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"vendor_technical_responses\" on \"vendor_technical_responses\".\"response_id\" = \"vendor_responses\".\"id\" left join \"vendor_commercial_responses\" on \"vendor_commercial_responses\".\"response_id\" = \"vendor_responses\".\"id\" left join \"rfq_evaluations\" on (\"rfq_evaluations\".\"rfq_id\" = \"vendor_responses\".\"rfq_id\" and \"rfq_evaluations\".\"vendor_id\" = \"vendor_responses\".\"vendor_id\" and \"rfq_evaluations\".\"eval_type\" = 'TBE') left join \"cbe_evaluations\" on (\"cbe_evaluations\".\"rfq_id\" = \"vendor_responses\".\"rfq_id\" and \"cbe_evaluations\".\"vendor_id\" = \"vendor_responses\".\"vendor_id\")", + "name": "vendor_responses_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_rfq_view": { + "columns": {}, + "definition": "select \"vendors\".\"id\" as \"vendor_id\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"address\" as \"address\", \"vendors\".\"country\" as \"country\", \"vendors\".\"email\" as \"email\", \"vendors\".\"website\" as \"website\", \"vendors\".\"status\" as \"vendor_status\", \"vendor_responses\".\"rfq_id\" as \"rfq_id\", \"vendor_responses\".\"response_status\" as \"rfq_vendor_status\", \"vendor_responses\".\"updated_at\" as \"rfq_vendor_updated\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"description\" as \"description\", \"rfqs\".\"due_date\" as \"due_date\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\" from \"vendors\" left join \"vendor_responses\" on \"vendor_responses\".\"vendor_id\" = \"vendors\".\"id\" left join \"rfqs\" on \"vendor_responses\".\"rfq_id\" = \"rfqs\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\"", + "name": "vendor_rfq_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_tbe_view": { + "columns": {}, + "definition": "select \"vendors\".\"id\" as \"vendor_id\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"address\" as \"address\", \"vendors\".\"country\" as \"country\", \"vendors\".\"email\" as \"email\", \"vendors\".\"website\" as \"website\", \"vendors\".\"status\" as \"vendor_status\", \"vendor_responses\".\"id\" as \"vendor_response_id\", \"vendor_responses\".\"rfq_id\" as \"rfq_id\", \"vendor_responses\".\"response_status\" as \"rfq_vendor_status\", \"vendor_responses\".\"updated_at\" as \"rfq_vendor_updated\", \"vendor_technical_responses\".\"id\" as \"technical_response_id\", \"vendor_technical_responses\".\"response_status\" as \"technical_response_status\", \"vendor_technical_responses\".\"summary\" as \"technical_summary\", \"vendor_technical_responses\".\"notes\" as \"technical_notes\", \"vendor_technical_responses\".\"updated_at\" as \"technical_updated\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"rfqs\".\"rfq_type\" as \"rfq_type\", \"rfqs\".\"status\" as \"rfq_status\", \"rfqs\".\"description\" as \"description\", \"rfqs\".\"due_date\" as \"due_date\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"rfq_evaluations\".\"id\" as \"tbe_id\", \"rfq_evaluations\".\"result\" as \"tbe_result\", \"rfq_evaluations\".\"notes\" as \"tbe_note\", \"rfq_evaluations\".\"updated_at\" as \"tbe_updated\" from \"vendors\" left join \"vendor_responses\" on \"vendor_responses\".\"vendor_id\" = \"vendors\".\"id\" left join \"rfqs\" on \"vendor_responses\".\"rfq_id\" = \"rfqs\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"vendor_technical_responses\" on \"vendor_technical_responses\".\"response_id\" = \"vendor_responses\".\"id\" left join \"rfq_evaluations\" on (\"rfq_evaluations\".\"vendor_id\" = \"vendors\".\"id\" and \"rfq_evaluations\".\"eval_type\" = 'TBE' and \"rfq_evaluations\".\"rfq_id\" = \"vendor_responses\".\"rfq_id\")", + "name": "vendor_tbe_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.role_view": { + "columns": {}, + "definition": "select \"roles\".\"id\" as \"id\", \"roles\".\"name\" as \"name\", \"roles\".\"description\" as \"description\", \"roles\".\"domain\" as \"domain\", \"roles\".\"created_at\" as \"created_at\", \"vendors\".\"id\" as \"company_id\", \"vendors\".\"vendor_name\" as \"company_name\", COUNT(\"users\".\"id\") as \"user_count\" from \"roles\" left join \"user_roles\" on \"user_roles\".\"role_id\" = \"roles\".\"id\" left join \"users\" on \"users\".\"id\" = \"user_roles\".\"user_id\" left join \"vendors\" on \"roles\".\"company_id\" = \"vendors\".\"id\" group by \"roles\".\"id\", \"vendors\".\"id\"", + "name": "role_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.user_view": { + "columns": {}, + "definition": "select \"users\".\"id\" as \"user_id\", \"users\".\"name\" as \"user_name\", \"users\".\"email\" as \"user_email\", \"users\".\"domain\" as \"user_domain\", \"users\".\"image_url\" as \"user_image\", \"vendors\".\"id\" as \"company_id\", \"vendors\".\"vendor_name\" as \"company_name\", \n array_agg(\"roles\".\"name\")\n as \"roles\", \"users\".\"created_at\" as \"created_at\" from \"users\" left join \"vendors\" on \"users\".\"company_id\" = \"vendors\".\"id\" left join \"user_roles\" on \"users\".\"id\" = \"user_roles\".\"user_id\" left join \"roles\" on \"user_roles\".\"role_id\" = \"roles\".\"id\" group by \"users\".\"id\", \"vendors\".\"id\"", + "name": "user_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.form_lists_view": { + "columns": {}, + "definition": "select \"tag_type_class_form_mappings\".\"id\" as \"id\", \"tag_type_class_form_mappings\".\"project_id\" as \"project_id\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"tag_type_class_form_mappings\".\"tag_type_label\" as \"tag_type_label\", \"tag_type_class_form_mappings\".\"class_label\" as \"class_label\", \"tag_type_class_form_mappings\".\"form_code\" as \"form_code\", \"tag_type_class_form_mappings\".\"form_name\" as \"form_name\", \"tag_type_class_form_mappings\".\"ep\" as \"ep\", \"tag_type_class_form_mappings\".\"remark\" as \"remark\", \"tag_type_class_form_mappings\".\"created_at\" as \"created_at\", \"tag_type_class_form_mappings\".\"updated_at\" as \"updated_at\" from \"tag_type_class_form_mappings\" inner join \"projects\" on \"tag_type_class_form_mappings\".\"project_id\" = \"projects\".\"id\"", + "name": "form_lists_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.view_tag_subfields": { + "columns": { + "tag_type_code": { + "name": "tag_type_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attributes_id": { + "name": "attributes_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "attributes_description": { + "name": "attributes_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expression": { + "name": "expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delimiter": { + "name": "delimiter", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "definition": "select \"tag_subfields\".\"id\" as \"id\", \"tag_subfields\".\"tag_type_code\", \"tag_types\".\"description\", \"tag_subfields\".\"attributes_id\", \"tag_subfields\".\"attributes_description\", \"tag_subfields\".\"expression\", \"tag_subfields\".\"delimiter\", \"tag_subfields\".\"sort_order\", \"tag_subfields\".\"created_at\", \"tag_subfields\".\"updated_at\", \"projects\".\"id\" as \"project_id\", \"projects\".\"code\", \"projects\".\"name\" from \"tag_subfields\" inner join \"tag_types\" on (\"tag_subfields\".\"tag_type_code\" = \"tag_types\".\"code\" and \"tag_subfields\".\"project_id\" = \"tag_types\".\"project_id\") inner join \"projects\" on \"tag_subfields\".\"project_id\" = \"projects\".\"id\"", + "name": "view_tag_subfields", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.document_stages_view": { + "columns": { + "document_id": { + "name": "document_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "doc_number": { + "name": "doc_number", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "issued_date": { + "name": "issued_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "contract_id": { + "name": "contract_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "stage_count": { + "name": "stage_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "stage_list": { + "name": "stage_list", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "definition": "\n SELECT\n d.id AS document_id,\n d.doc_number,\n d.title,\n d.status,\n d.issued_date,\n d.contract_id,\n (SELECT COUNT(*) FROM issue_stages WHERE document_id = d.id) AS stage_count,\n COALESCE( \n (SELECT json_agg(i.stage_name) FROM issue_stages i WHERE i.document_id = d.id), \n '[]'\n ) AS stage_list,\n d.created_at,\n d.updated_at\n FROM documents d\n", + "name": "document_stages_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.enhanced_documents_view": { + "columns": { + "document_id": { + "name": "document_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "doc_number": { + "name": "doc_number", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "drawing_kind": { + "name": "drawing_kind", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "vendor_doc_number": { + "name": "vendor_doc_number", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "pic": { + "name": "pic", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "issued_date": { + "name": "issued_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "contract_id": { + "name": "contract_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_code": { + "name": "project_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "c_gbn": { + "name": "c_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "d_gbn": { + "name": "d_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "degree_gbn": { + "name": "degree_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "dept_gbn": { + "name": "dept_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "j_gbn": { + "name": "j_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "s_gbn": { + "name": "s_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "current_stage_id": { + "name": "current_stage_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "current_stage_name": { + "name": "current_stage_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "current_stage_status": { + "name": "current_stage_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "current_stage_order": { + "name": "current_stage_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "current_stage_plan_date": { + "name": "current_stage_plan_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "current_stage_actual_date": { + "name": "current_stage_actual_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "current_stage_assignee_name": { + "name": "current_stage_assignee_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "current_stage_priority": { + "name": "current_stage_priority", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "days_until_due": { + "name": "days_until_due", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_overdue": { + "name": "is_overdue", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "days_difference": { + "name": "days_difference", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_stages": { + "name": "total_stages", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "completed_stages": { + "name": "completed_stages", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "progress_percentage": { + "name": "progress_percentage", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latest_revision": { + "name": "latest_revision", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "latest_revision_status": { + "name": "latest_revision_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "latest_revision_uploader_name": { + "name": "latest_revision_uploader_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "latest_submitted_date": { + "name": "latest_submitted_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "all_stages": { + "name": "all_stages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attachment_count": { + "name": "attachment_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "definition": "\n WITH document_stats AS (\n SELECT \n d.id as document_id,\n COUNT(ist.id) as total_stages,\n COUNT(CASE WHEN ist.stage_status IN ('COMPLETED', 'APPROVED') THEN 1 END) as completed_stages,\n CASE \n WHEN COUNT(ist.id) > 0 \n THEN ROUND((COUNT(CASE WHEN ist.stage_status IN ('COMPLETED', 'APPROVED') THEN 1 END) * 100.0) / COUNT(ist.id))\n ELSE 0 \n END as progress_percentage\n FROM documents d\n LEFT JOIN issue_stages ist ON d.id = ist.document_id\n GROUP BY d.id\n ),\n current_stage_info AS (\n SELECT DISTINCT ON (document_id)\n document_id,\n id as current_stage_id,\n stage_name as current_stage_name,\n stage_status as current_stage_status,\n stage_order as current_stage_order,\n plan_date as current_stage_plan_date,\n actual_date as current_stage_actual_date,\n assignee_name as current_stage_assignee_name,\n priority as current_stage_priority,\n CASE \n WHEN actual_date IS NULL AND plan_date IS NOT NULL \n THEN plan_date - CURRENT_DATE\n ELSE NULL \n END as days_until_due,\n CASE \n WHEN actual_date IS NULL AND plan_date < CURRENT_DATE \n THEN true\n WHEN actual_date IS NOT NULL AND actual_date > plan_date \n THEN true\n ELSE false \n END as is_overdue,\n CASE \n WHEN actual_date IS NOT NULL AND plan_date IS NOT NULL \n THEN actual_date - plan_date\n ELSE NULL \n END as days_difference\n FROM issue_stages\n WHERE stage_status NOT IN ('COMPLETED', 'APPROVED')\n ORDER BY document_id, stage_order ASC, priority DESC\n ),\n latest_revision_info AS (\n SELECT DISTINCT ON (ist.document_id)\n ist.document_id,\n r.id as latest_revision_id,\n r.revision as latest_revision,\n r.revision_status as latest_revision_status,\n r.uploader_name as latest_revision_uploader_name,\n r.submitted_date as latest_submitted_date\n FROM revisions r\n JOIN issue_stages ist ON r.issue_stage_id = ist.id\n ORDER BY ist.document_id, r.created_at DESC\n ),\n -- 리비전별 첨부파일 집계\n revision_attachments AS (\n SELECT \n r.id as revision_id,\n COALESCE(\n json_agg(\n json_build_object(\n 'id', da.id,\n 'revisionId', da.revision_id,\n 'fileName', da.file_name,\n 'filePath', da.file_path,\n 'fileSize', da.file_size,\n 'fileType', da.file_type,\n 'createdAt', da.created_at,\n 'updatedAt', da.updated_at\n ) ORDER BY da.created_at\n ) FILTER (WHERE da.id IS NOT NULL),\n '[]'::json\n ) as attachments\n FROM revisions r\n LEFT JOIN document_attachments da ON r.id = da.revision_id\n GROUP BY r.id\n ),\n -- 스테이지별 리비전 집계 (첨부파일 포함)\n stage_revisions AS (\n SELECT \n ist.id as stage_id,\n COALESCE(\n json_agg(\n json_build_object(\n 'id', r.id,\n 'issueStageId', r.issue_stage_id,\n 'revision', r.revision,\n 'uploaderType', r.uploader_type,\n 'uploaderId', r.uploader_id,\n 'uploaderName', r.uploader_name,\n 'comment', r.comment,\n 'usage', r.usage,\n 'revisionStatus', r.revision_status,\n 'submittedDate', r.submitted_date,\n 'uploadedAt', r.uploaded_at,\n 'approvedDate', r.approved_date,\n 'reviewStartDate', r.review_start_date,\n 'rejectedDate', r.rejected_date,\n 'reviewerId', r.reviewer_id,\n 'reviewerName', r.reviewer_name,\n 'reviewComments', r.review_comments,\n 'createdAt', r.created_at,\n 'updatedAt', r.updated_at,\n 'attachments', ra.attachments\n ) ORDER BY r.created_at\n ) FILTER (WHERE r.id IS NOT NULL),\n '[]'::json\n ) as revisions\n FROM issue_stages ist\n LEFT JOIN revisions r ON ist.id = r.issue_stage_id\n LEFT JOIN revision_attachments ra ON r.id = ra.revision_id\n GROUP BY ist.id\n ),\n -- 문서별 스테이지 집계 (리비전 포함)\n stage_aggregation AS (\n SELECT \n ist.document_id,\n json_agg(\n json_build_object(\n 'id', ist.id,\n 'stageName', ist.stage_name,\n 'stageStatus', ist.stage_status,\n 'stageOrder', ist.stage_order,\n 'planDate', ist.plan_date,\n 'actualDate', ist.actual_date,\n 'assigneeName', ist.assignee_name,\n 'priority', ist.priority,\n 'revisions', sr.revisions\n ) ORDER BY ist.stage_order\n ) as all_stages\n FROM issue_stages ist\n LEFT JOIN stage_revisions sr ON ist.id = sr.stage_id\n GROUP BY ist.document_id\n ),\n attachment_counts AS (\n SELECT \n ist.document_id,\n COUNT(da.id) as attachment_count\n FROM issue_stages ist\n LEFT JOIN revisions r ON ist.id = r.issue_stage_id\n LEFT JOIN document_attachments da ON r.id = da.revision_id\n GROUP BY ist.document_id\n )\n \n SELECT \n d.id as document_id,\n d.doc_number,\n d.drawing_kind,\n d.vendor_doc_number, -- ✅ 벤더 문서 번호 추가\n d.title,\n d.pic,\n d.status,\n d.issued_date,\n d.contract_id,\n\n d.c_gbn,\n d.d_gbn,\n d.degree_gbn,\n d.dept_gbn,\n d.s_gbn,\n d.j_gbn,\n\n\n \n -- ✅ 프로젝트 및 벤더 정보 추가\n p.code as project_code,\n v.vendor_name as vendor_name,\n v.vendor_code as vendor_code,\n \n -- 현재 스테이지 정보\n csi.current_stage_id,\n csi.current_stage_name,\n csi.current_stage_status,\n csi.current_stage_order,\n csi.current_stage_plan_date,\n csi.current_stage_actual_date,\n csi.current_stage_assignee_name,\n csi.current_stage_priority,\n \n -- 계산 필드\n csi.days_until_due,\n csi.is_overdue,\n csi.days_difference,\n \n -- 진행률 정보\n ds.total_stages,\n ds.completed_stages,\n ds.progress_percentage,\n \n -- 최신 리비전 정보\n lri.latest_revision_id,\n lri.latest_revision,\n lri.latest_revision_status,\n lri.latest_revision_uploader_name,\n lri.latest_submitted_date,\n \n -- 전체 스테이지 (리비전 및 첨부파일 포함)\n COALESCE(sa.all_stages, '[]'::json) as all_stages,\n \n -- 기타\n COALESCE(ac.attachment_count, 0) as attachment_count,\n d.created_at,\n d.updated_at\n \n FROM documents d\n -- ✅ contracts, projects, vendors 테이블 JOIN 추가\n LEFT JOIN contracts c ON d.contract_id = c.id\n LEFT JOIN projects p ON c.project_id = p.id\n LEFT JOIN vendors v ON c.vendor_id = v.id\n \n LEFT JOIN document_stats ds ON d.id = ds.document_id\n LEFT JOIN current_stage_info csi ON d.id = csi.document_id\n LEFT JOIN latest_revision_info lri ON d.id = lri.document_id\n LEFT JOIN stage_aggregation sa ON d.id = sa.document_id\n LEFT JOIN attachment_counts ac ON d.id = ac.document_id\n \n ORDER BY d.created_at DESC\n", + "name": "enhanced_documents_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.simplified_documents_view": { + "columns": { + "document_id": { + "name": "document_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "doc_number": { + "name": "doc_number", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "drawing_kind": { + "name": "drawing_kind", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "vendor_doc_number": { + "name": "vendor_doc_number", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "pic": { + "name": "pic", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "issued_date": { + "name": "issued_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "contract_id": { + "name": "contract_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_code": { + "name": "project_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "c_gbn": { + "name": "c_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "d_gbn": { + "name": "d_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "degree_gbn": { + "name": "degree_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "dept_gbn": { + "name": "dept_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "j_gbn": { + "name": "j_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "s_gbn": { + "name": "s_gbn", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "first_stage_id": { + "name": "first_stage_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "first_stage_name": { + "name": "first_stage_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "first_stage_plan_date": { + "name": "first_stage_plan_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "first_stage_actual_date": { + "name": "first_stage_actual_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "second_stage_id": { + "name": "second_stage_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "second_stage_name": { + "name": "second_stage_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "second_stage_plan_date": { + "name": "second_stage_plan_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "second_stage_actual_date": { + "name": "second_stage_actual_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "all_stages": { + "name": "all_stages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attachment_count": { + "name": "attachment_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "definition": "\n WITH \n -- 리비전별 첨부파일 집계\n revision_attachments AS (\n SELECT \n r.id as revision_id,\n COALESCE(\n json_agg(\n json_build_object(\n 'id', da.id,\n 'revisionId', da.revision_id,\n 'fileName', da.file_name,\n 'filePath', da.file_path,\n 'fileSize', da.file_size,\n 'fileType', da.file_type,\n 'createdAt', da.created_at,\n 'updatedAt', da.updated_at\n ) ORDER BY da.created_at\n ) FILTER (WHERE da.id IS NOT NULL),\n '[]'::json\n ) as attachments\n FROM revisions r\n LEFT JOIN document_attachments da ON r.id = da.revision_id\n GROUP BY r.id\n ),\n -- 스테이지별 리비전 집계 (첨부파일 포함)\n stage_revisions AS (\n SELECT \n ist.id as stage_id,\n COALESCE(\n json_agg(\n json_build_object(\n 'id', r.id,\n 'issueStageId', r.issue_stage_id,\n 'revision', r.revision,\n 'uploaderType', r.uploader_type,\n 'uploaderId', r.uploader_id,\n 'uploaderName', r.uploader_name,\n 'comment', r.comment,\n 'usage', r.usage,\n 'usageType', r.usage_type,\n 'revisionStatus', r.revision_status,\n 'submittedDate', r.submitted_date,\n 'uploadedAt', r.uploaded_at,\n 'approvedDate', r.approved_date,\n 'reviewStartDate', r.review_start_date,\n 'rejectedDate', r.rejected_date,\n 'reviewerId', r.reviewer_id,\n 'reviewerName', r.reviewer_name,\n 'reviewComments', r.review_comments,\n 'createdAt', r.created_at,\n 'updatedAt', r.updated_at,\n 'attachments', ra.attachments\n ) ORDER BY r.created_at\n ) FILTER (WHERE r.id IS NOT NULL),\n '[]'::json\n ) as revisions\n FROM issue_stages ist\n LEFT JOIN revisions r ON ist.id = r.issue_stage_id\n LEFT JOIN revision_attachments ra ON r.id = ra.revision_id\n GROUP BY ist.id\n ),\n -- 문서별 스테이지 집계 (리비전 포함)\n stage_aggregation AS (\n SELECT \n ist.document_id,\n json_agg(\n json_build_object(\n 'id', ist.id,\n 'stageName', ist.stage_name,\n 'stageStatus', ist.stage_status,\n 'stageOrder', ist.stage_order,\n 'planDate', ist.plan_date,\n 'actualDate', ist.actual_date,\n 'assigneeName', ist.assignee_name,\n 'priority', ist.priority,\n 'revisions', sr.revisions\n ) ORDER BY ist.stage_order\n ) as all_stages\n FROM issue_stages ist\n LEFT JOIN stage_revisions sr ON ist.id = sr.stage_id\n GROUP BY ist.document_id\n ),\n -- 첫 번째 스테이지 정보 (drawingKind에 따라 다른 조건)\n first_stage_info AS (\n SELECT DISTINCT ON (ist.document_id)\n ist.document_id,\n ist.id as first_stage_id,\n ist.stage_name as first_stage_name,\n ist.plan_date as first_stage_plan_date,\n ist.actual_date as first_stage_actual_date\n FROM issue_stages ist\n JOIN documents d ON ist.document_id = d.id\n WHERE \n (d.drawing_kind = 'B4' AND LOWER(ist.stage_name) LIKE '%pre%') OR\n (d.drawing_kind = 'B3' AND LOWER(ist.stage_name) LIKE '%approval%') OR\n (d.drawing_kind = 'B5' AND LOWER(ist.stage_name) LIKE '%first%')\n ORDER BY ist.document_id, ist.stage_order ASC\n ),\n -- 두 번째 스테이지 정보 (drawingKind에 따라 다른 조건)\n second_stage_info AS (\n SELECT DISTINCT ON (ist.document_id)\n ist.document_id,\n ist.id as second_stage_id,\n ist.stage_name as second_stage_name,\n ist.plan_date as second_stage_plan_date,\n ist.actual_date as second_stage_actual_date\n FROM issue_stages ist\n JOIN documents d ON ist.document_id = d.id\n WHERE \n (d.drawing_kind = 'B4' AND LOWER(ist.stage_name) LIKE '%work%') OR\n (d.drawing_kind = 'B3' AND LOWER(ist.stage_name) LIKE '%work%') OR\n (d.drawing_kind = 'B5' AND LOWER(ist.stage_name) LIKE '%second%')\n ORDER BY ist.document_id, ist.stage_order ASC\n ),\n -- 첨부파일 수 집계\n attachment_counts AS (\n SELECT \n ist.document_id,\n COUNT(da.id) as attachment_count\n FROM issue_stages ist\n LEFT JOIN revisions r ON ist.id = r.issue_stage_id\n LEFT JOIN document_attachments da ON r.id = da.revision_id\n GROUP BY ist.document_id\n )\n \n SELECT \n d.id as document_id,\n d.doc_number,\n d.drawing_kind,\n d.vendor_doc_number,\n d.title,\n d.pic,\n d.status,\n d.issued_date,\n d.contract_id,\n \n -- B4 전용 필드들\n d.c_gbn,\n d.d_gbn,\n d.degree_gbn,\n d.dept_gbn,\n d.s_gbn,\n d.j_gbn,\n \n -- 프로젝트 및 벤더 정보\n p.code as project_code,\n v.vendor_name as vendor_name,\n v.vendor_code as vendor_code,\n \n -- 첫 번째 스테이지 정보\n fsi.first_stage_id,\n fsi.first_stage_name,\n fsi.first_stage_plan_date,\n fsi.first_stage_actual_date,\n \n -- 두 번째 스테이지 정보\n ssi.second_stage_id,\n ssi.second_stage_name,\n ssi.second_stage_plan_date,\n ssi.second_stage_actual_date,\n \n -- 전체 스테이지 (리비전 및 첨부파일 포함)\n COALESCE(sa.all_stages, '[]'::json) as all_stages,\n \n -- 기타\n COALESCE(ac.attachment_count, 0) as attachment_count,\n d.created_at,\n d.updated_at\n \n FROM documents d\n -- contracts, projects, vendors 테이블 JOIN\n LEFT JOIN contracts c ON d.contract_id = c.id\n INNER JOIN projects p ON c.project_id = p.id AND p.type = 'ship'\n LEFT JOIN vendors v ON c.vendor_id = v.id\n \n -- 스테이지 정보 JOIN\n LEFT JOIN first_stage_info fsi ON d.id = fsi.document_id\n LEFT JOIN second_stage_info ssi ON d.id = ssi.document_id\n LEFT JOIN stage_aggregation sa ON d.id = sa.document_id\n LEFT JOIN attachment_counts ac ON d.id = ac.document_id\n \n ORDER BY d.created_at DESC\n", + "name": "simplified_documents_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.sync_status_view": { + "columns": { + "contract_id": { + "name": "contract_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_system": { + "name": "target_system", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "total_changes": { + "name": "total_changes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pending_changes": { + "name": "pending_changes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "synced_changes": { + "name": "synced_changes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "failed_changes": { + "name": "failed_changes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sync_enabled": { + "name": "sync_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n WITH change_stats AS (\n SELECT \n cl.contract_id,\n sc.target_system,\n COUNT(*) as total_changes,\n COUNT(CASE WHEN cl.is_synced = false AND cl.sync_attempts < sc.retry_max_attempts THEN 1 END) as pending_changes,\n COUNT(CASE WHEN cl.is_synced = true THEN 1 END) as synced_changes,\n COUNT(CASE WHEN cl.sync_attempts >= sc.retry_max_attempts AND cl.is_synced = false THEN 1 END) as failed_changes,\n MAX(cl.synced_at) as last_sync_at\n FROM change_logs cl\n CROSS JOIN sync_configs sc \n WHERE cl.contract_id = sc.contract_id\n AND (cl.target_systems IS NULL OR cl.target_systems @> to_jsonb(sc.target_system))\n GROUP BY cl.contract_id, sc.target_system\n )\n SELECT \n cs.contract_id,\n cs.target_system,\n COALESCE(cs.total_changes, 0) as total_changes,\n COALESCE(cs.pending_changes, 0) as pending_changes,\n COALESCE(cs.synced_changes, 0) as synced_changes,\n COALESCE(cs.failed_changes, 0) as failed_changes,\n cs.last_sync_at,\n CASE \n WHEN sc.sync_enabled = true AND sc.last_successful_sync IS NOT NULL \n THEN sc.last_successful_sync + (sc.sync_interval_minutes || ' minutes')::interval\n ELSE NULL\n END as next_sync_at,\n sc.sync_enabled\n FROM sync_configs sc\n LEFT JOIN change_stats cs ON sc.contract_id = cs.contract_id AND sc.target_system = cs.target_system\n", + "name": "sync_status_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_documents_view": { + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "doc_number": { + "name": "doc_number", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "pic": { + "name": "pic", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "issued_date": { + "name": "issued_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "contract_id": { + "name": "contract_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "latest_stage_id": { + "name": "latest_stage_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latest_stage_name": { + "name": "latest_stage_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "latest_stage_plan_date": { + "name": "latest_stage_plan_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "latest_stage_actual_date": { + "name": "latest_stage_actual_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latest_revision": { + "name": "latest_revision", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "latest_revision_uploader_type": { + "name": "latest_revision_uploader_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "latest_revision_uploader_name": { + "name": "latest_revision_uploader_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "attachment_count": { + "name": "attachment_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "definition": "\n SELECT \n d.id, \n d.doc_number,\n d.title,\n d.pic,\n d.status,\n d.issued_date,\n d.contract_id,\n \n (SELECT id FROM issue_stages WHERE document_id = d.id ORDER BY created_at DESC LIMIT 1) AS latest_stage_id,\n (SELECT stage_name FROM issue_stages WHERE document_id = d.id ORDER BY created_at DESC LIMIT 1) AS latest_stage_name,\n (SELECT plan_date FROM issue_stages WHERE document_id = d.id ORDER BY created_at DESC LIMIT 1) AS latest_stage_plan_date,\n (SELECT actual_date FROM issue_stages WHERE document_id = d.id ORDER BY created_at DESC LIMIT 1) AS latest_stage_actual_date,\n \n (SELECT r.id FROM revisions r JOIN issue_stages i ON r.issue_stage_id = i.id WHERE i.document_id = d.id ORDER BY r.created_at DESC LIMIT 1) AS latest_revision_id,\n (SELECT r.revision FROM revisions r JOIN issue_stages i ON r.issue_stage_id = i.id WHERE i.document_id = d.id ORDER BY r.created_at DESC LIMIT 1) AS latest_revision,\n (SELECT r.uploader_type FROM revisions r JOIN issue_stages i ON r.issue_stage_id = i.id WHERE i.document_id = d.id ORDER BY r.created_at DESC LIMIT 1) AS latest_revision_uploader_type,\n (SELECT r.uploader_name FROM revisions r JOIN issue_stages i ON r.issue_stage_id = i.id WHERE i.document_id = d.id ORDER BY r.created_at DESC LIMIT 1) AS latest_revision_uploader_name,\n \n (SELECT COUNT(*) FROM document_attachments a JOIN revisions r ON a.revision_id = r.id JOIN issue_stages i ON r.issue_stage_id = i.id WHERE i.document_id = d.id) AS attachment_count,\n \n d.created_at,\n d.updated_at\n FROM documents d\n JOIN contracts c ON d.contract_id = c.id\n ", + "name": "vendor_documents_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_candidates_with_vendor_info": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "company_name": { + "name": "company_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "contact_email": { + "name": "contact_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "contact_phone": { + "name": "contact_phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'COLLECTED'" + }, + "items": { + "name": "items", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"vendor_candidates\".\"id\", \"vendor_candidates\".\"company_name\", \"vendor_candidates\".\"contact_email\", \"vendor_candidates\".\"contact_phone\", \"vendor_candidates\".\"tax_id\", \"vendor_candidates\".\"address\", \"vendor_candidates\".\"country\", \"vendor_candidates\".\"source\", \"vendor_candidates\".\"status\", \"vendor_candidates\".\"items\", \"vendor_candidates\".\"remark\", \"vendor_candidates\".\"created_at\", \"vendor_candidates\".\"updated_at\", \"vendors\".\"vendor_name\", \"vendors\".\"vendor_code\", \"vendors\".\"created_at\" as \"vendor_created_at\", (\n SELECT l2.\"created_at\"\n FROM \"vendor_candidate_logs\" l2\n WHERE l2.\"vendor_candidate_id\" = \"vendor_candidates\".\"id\"\n AND l2.\"action\" = 'status_change'\n ORDER BY l2.\"created_at\" DESC\n LIMIT 1\n ) as \"last_status_change_at\", (\n SELECT u.\"name\"\n FROM \"users\" u\n JOIN \"vendor_candidate_logs\" l3\n ON l3.\"user_id\" = u.\"id\"\n WHERE l3.\"vendor_candidate_id\" = \"vendor_candidates\".\"id\"\n AND l3.\"action\" = 'status_change'\n ORDER BY l3.\"created_at\" DESC\n LIMIT 1\n ) as \"last_status_change_by\", (\n SELECT l4.\"created_at\"\n FROM \"vendor_candidate_logs\" l4\n WHERE l4.\"vendor_candidate_id\" = \"vendor_candidates\".\"id\"\n AND l4.\"action\" = 'invite_sent'\n ORDER BY l4.\"created_at\" DESC\n LIMIT 1\n ) as \"last_invitation_at\", (\n SELECT u2.\"name\"\n FROM \"users\" u2\n JOIN \"vendor_candidate_logs\" l5\n ON l5.\"user_id\" = u2.\"id\"\n WHERE l5.\"vendor_candidate_id\" = \"vendor_candidates\".\"id\"\n AND l5.\"action\" = 'invite_sent'\n ORDER BY l5.\"created_at\" DESC\n LIMIT 1\n ) as \"last_invitation_by\" from \"vendor_candidates\" left join \"vendors\" on \"vendor_candidates\".\"vendor_id\" = \"vendors\".\"id\"", + "name": "vendor_candidates_with_vendor_info", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_detail_view": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "business_size": { + "name": "business_size", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'PENDING_REVIEW'" + }, + "representative_name": { + "name": "representative_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "representative_birth": { + "name": "representative_birth", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "representative_email": { + "name": "representative_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "representative_phone": { + "name": "representative_phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "corporate_registration_number": { + "name": "corporate_registration_number", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "credit_agency": { + "name": "credit_agency", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "credit_rating": { + "name": "credit_rating", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "cash_flow_rating": { + "name": "cash_flow_rating", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "definition": "select \"id\", \"vendor_name\", \"vendor_code\", \"tax_id\", \"address\", \"business_size\", \"country\", \"phone\", \"email\", \"website\", \"status\", \"representative_name\", \"representative_birth\", \"representative_email\", \"representative_phone\", \"corporate_registration_number\", \"credit_agency\", \"credit_rating\", \"cash_flow_rating\", \"created_at\", \"updated_at\", \n (SELECT COALESCE(\n json_agg(\n json_build_object(\n 'id', c.id,\n 'contactName', c.contact_name,\n 'contactPosition', c.contact_position,\n 'contactEmail', c.contact_email,\n 'contactPhone', c.contact_phone,\n 'isPrimary', c.is_primary\n )\n ),\n '[]'::json\n )\n FROM vendor_contacts c\n WHERE c.vendor_id = vendors.id)\n as \"contacts\", \n (SELECT COALESCE(\n json_agg(\n json_build_object(\n 'id', a.id,\n 'fileName', a.file_name,\n 'filePath', a.file_path,\n 'attachmentType', a.attachment_type,\n 'createdAt', a.created_at\n )\n ORDER BY a.attachment_type, a.created_at DESC\n ),\n '[]'::json\n )\n FROM vendor_attachments a\n WHERE a.vendor_id = vendors.id)\n as \"attachments\", \n (SELECT COUNT(*)\n FROM vendor_attachments a\n WHERE a.vendor_id = vendors.id)\n as \"attachment_count\", \n (SELECT COUNT(*) \n FROM vendor_contacts c\n WHERE c.vendor_id = vendors.id)\n as \"contact_count\" from \"vendors\"", + "name": "vendor_detail_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_items_view": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_name": { + "name": "item_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "definition": "select \"vendor_possible_items\".\"id\", \"vendor_possible_items\".\"vendor_id\", \"items\".\"item_name\", \"items\".\"item_code\", \"items\".\"description\", \"vendor_possible_items\".\"created_at\", \"vendor_possible_items\".\"updated_at\" from \"vendor_possible_items\" left join \"items\" on \"vendor_possible_items\".\"item_code\" = \"items\".\"item_code\"", + "name": "vendor_items_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_materials_view": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_name": { + "name": "item_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit_of_measure": { + "name": "unit_of_measure", + "type": "varchar(3)", + "primaryKey": false, + "notNull": false + }, + "steel_type": { + "name": "steel_type", + "type": "varchar(2)", + "primaryKey": false, + "notNull": false + }, + "grade_material": { + "name": "grade_material", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "definition": "select \"vendor_possible_materials\".\"id\", \"vendor_possible_materials\".\"vendor_id\", \"materials\".\"item_name\", \"materials\".\"item_code\", \"materials\".\"description\", \"materials\".\"unit_of_measure\", \"materials\".\"steel_type\", \"materials\".\"grade_material\", \"vendor_possible_materials\".\"created_at\", \"vendor_possible_materials\".\"updated_at\" from \"vendor_possible_materials\" left join \"materials\" on \"vendor_possible_materials\".\"item_code\" = \"materials\".\"item_code\"", + "name": "vendor_materials_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendors_with_types": { + "columns": {}, + "definition": "select \"vendors\".\"id\" as \"id\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"tax_id\" as \"tax_id\", \"vendors\".\"address\" as \"address\", \"vendors\".\"country\" as \"country\", \"vendors\".\"phone\" as \"phone\", \"vendors\".\"email\" as \"email\", \"vendors\".\"business_size\" as \"business_size\", \"vendors\".\"website\" as \"website\", \"vendors\".\"status\" as \"status\", \"vendors\".\"vendor_type_id\" as \"vendor_type_id\", \"vendors\".\"representative_name\" as \"representative_name\", \"vendors\".\"representative_birth\" as \"representative_birth\", \"vendors\".\"representative_email\" as \"representative_email\", \"vendors\".\"representative_phone\" as \"representative_phone\", \"vendors\".\"corporate_registration_number\" as \"corporate_registration_number\", \"vendors\".\"items\" as \"items\", \"vendors\".\"credit_agency\" as \"credit_agency\", \"vendors\".\"credit_rating\" as \"credit_rating\", \"vendors\".\"cash_flow_rating\" as \"cash_flow_rating\", \"vendors\".\"created_at\" as \"created_at\", \"vendors\".\"updated_at\" as \"updated_at\", \"vendor_types\".\"name_ko\" as \"vendor_type_name\", \"vendor_types\".\"name_en\" as \"vendor_type_name_en\", \"vendor_types\".\"code\" as \"vendor_type_code\", \n CASE\n WHEN \"vendors\".\"status\" = 'ACTIVE' THEN '정규업체'\n WHEN \"vendors\".\"status\" IN ('INACTIVE', 'BLACKLISTED', 'REJECTED') THEN ''\n ELSE '잠재업체'\n END\n as \"vendor_category\" from \"vendors\" left join \"vendor_types\" on \"vendors\".\"vendor_type_id\" = \"vendor_types\".\"id\"", + "name": "vendors_with_types", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.basic_contract_view": { + "columns": {}, + "definition": "select \"basic_contract\".\"id\" as \"id\", \"basic_contract\".\"template_id\" as \"template_id\", \"basic_contract\".\"vendor_id\" as \"vendor_id\", \"basic_contract\".\"requested_by\" as \"requested_by\", \"basic_contract\".\"status\" as \"basic_contract_status\", \"basic_contract\".\"created_at\" as \"created_at\", \"basic_contract\".\"updated_at\" as \"updated_at\", \"basic_contract\".\"updated_at\" as \"completed_at\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"email\" as \"vendor_email\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"users\".\"name\" as \"user_name\", \"basic_contract_templates\".\"template_name\" as \"template_name\", \"basic_contract_templates\".\"validity_period\" as \"validityPeriod\", \"basic_contract_templates\".\"file_path\" as \"file_path\", \"basic_contract_templates\".\"file_name\" as \"file_name\", \"basic_contract\".\"file_path\" as \"signed_file_path\" from \"basic_contract\" left join \"vendors\" on \"basic_contract\".\"vendor_id\" = \"vendors\".\"id\" left join \"users\" on \"basic_contract\".\"requested_by\" = \"users\".\"id\" left join \"basic_contract_templates\" on \"basic_contract\".\"template_id\" = \"basic_contract_templates\".\"id\"", + "name": "basic_contract_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.pr_items_view": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "procurement_rfqs_id": { + "name": "procurement_rfqs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_item": { + "name": "rfq_item", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "pr_item": { + "name": "pr_item", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "pr_no": { + "name": "pr_no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "material_code": { + "name": "material_code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "material_category": { + "name": "material_category", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "acc": { + "name": "acc", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "material_description": { + "name": "material_description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "uom": { + "name": "uom", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "gross_weight": { + "name": "gross_weight", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "gw_uom": { + "name": "gw_uom", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "spec_no": { + "name": "spec_no", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "spec_url": { + "name": "spec_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tracking_no": { + "name": "tracking_no", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "major_yn": { + "name": "major_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "project_def": { + "name": "project_def", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_sc": { + "name": "project_sc", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_kl": { + "name": "project_kl", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_lc": { + "name": "project_lc", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_dl": { + "name": "project_dl", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "item_name": { + "name": "item_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"pr_items\".\"id\", \"pr_items\".\"procurement_rfqs_id\", \"pr_items\".\"rfq_item\", \"pr_items\".\"pr_item\", \"pr_items\".\"pr_no\", \"pr_items\".\"material_code\", \"pr_items\".\"material_category\", \"pr_items\".\"acc\", \"pr_items\".\"material_description\", \"pr_items\".\"size\", \"pr_items\".\"delivery_date\", \"pr_items\".\"quantity\", \"pr_items\".\"uom\", \"pr_items\".\"gross_weight\", \"pr_items\".\"gw_uom\", \"pr_items\".\"spec_no\", \"pr_items\".\"spec_url\", \"pr_items\".\"tracking_no\", \"pr_items\".\"major_yn\", \"pr_items\".\"project_def\", \"pr_items\".\"project_sc\", \"pr_items\".\"project_kl\", \"pr_items\".\"project_lc\", \"pr_items\".\"project_dl\", \"pr_items\".\"remark\", \"procurement_rfqs\".\"rfq_code\", \"procurement_rfqs\".\"item_code\", \"procurement_rfqs\".\"item_name\" from \"pr_items\" left join \"procurement_rfqs\" on \"pr_items\".\"procurement_rfqs_id\" = \"procurement_rfqs\".\"id\"", + "name": "pr_items_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.procurement_rfq_details_view": { + "columns": {}, + "definition": "select \"rfq_details\".\"id\" as \"detail_id\", \"rfqs\".\"id\" as \"rfq_id\", \"rfqs\".\"rfq_code\" as \"rfq_code\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"rfqs\".\"item_code\" as \"item_code\", \"rfqs\".\"item_name\" as \"item_name\", \"vendors\".\"vendor_name\" as \"vendor_name\", \"vendors\".\"vendor_code\" as \"vendor_code\", \"vendors\".\"id\" as \"vendor_id\", \"vendors\".\"country\" as \"vendor_country\", \"rfq_details\".\"currency\" as \"currency\", \"payment_terms\".\"code\" as \"payment_terms_code\", \"payment_terms\".\"description\" as \"payment_terms_description\", \"incoterms\".\"code\" as \"incoterms_code\", \"incoterms\".\"description\" as \"incoterms_description\", \"rfq_details\".\"incoterms_detail\" as \"incoterms_detail\", \"rfq_details\".\"delivery_date\" as \"delivery_date\", \"rfq_details\".\"tax_code\" as \"tax_code\", \"rfq_details\".\"place_of_shipping\" as \"place_of_shipping\", \"rfq_details\".\"place_of_destination\" as \"place_of_destination\", \"rfq_details\".\"material_price_related_yn\" as \"material_price_related_yn\", \"updated_by_user\".\"name\" as \"updated_by_user_name\", \"rfq_details\".\"updated_at\" as \"updated_at\", (\n SELECT COUNT(*) \n FROM pr_items \n WHERE procurement_rfqs_id = \"rfqs\".\"id\"\n ) as \"pr_items_count\", (\n SELECT COUNT(*) \n FROM pr_items \n WHERE procurement_rfqs_id = \"rfqs\".\"id\" \n AND major_yn = true\n ) as \"major_items_count\", (\n SELECT COUNT(*) \n FROM procurement_rfq_comments \n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ) as \"comment_count\", (\n SELECT created_at \n FROM procurement_rfq_comments \n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ORDER BY created_at DESC LIMIT 1\n ) as \"last_comment_date\", (\n SELECT created_at \n FROM procurement_rfq_comments \n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\" AND is_vendor_comment = true\n ORDER BY created_at DESC LIMIT 1\n ) as \"last_vendor_comment_date\", (\n SELECT COUNT(*) \n FROM procurement_rfq_attachments \n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ) as \"attachment_count\", (\n SELECT COUNT(*) > 0\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ) as \"has_quotation\", (\n SELECT status\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ORDER BY created_at DESC LIMIT 1\n ) as \"quotation_status\", (\n SELECT total_price\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ORDER BY created_at DESC LIMIT 1\n ) as \"quotation_total_price\", (\n SELECT quotation_version\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ORDER BY quotation_version DESC LIMIT 1\n ) as \"quotation_version\", (\n SELECT COUNT(DISTINCT quotation_version)\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ) as \"quotation_version_count\", (\n SELECT created_at\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"rfqs\".\"id\" AND vendor_id = \"rfq_details\".\"vendors_id\"\n ORDER BY quotation_version DESC LIMIT 1\n ) as \"last_quotation_date\" from \"procurement_rfq_details\" \"rfq_details\" left join \"procurement_rfqs\" \"rfqs\" on \"rfq_details\".\"procurement_rfqs_id\" = \"rfqs\".\"id\" left join \"projects\" on \"rfqs\".\"project_id\" = \"projects\".\"id\" left join \"vendors\" on \"rfq_details\".\"vendors_id\" = \"vendors\".\"id\" left join \"payment_terms\" on \"rfq_details\".\"payment_terms_code\" = \"payment_terms\".\"code\" left join \"incoterms\" on \"rfq_details\".\"incoterms_code\" = \"incoterms\".\"code\" left join \"users\" \"updated_by_user\" on \"rfq_details\".\"updated_by\" = \"updated_by_user\".\"id\"", + "name": "procurement_rfq_details_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.procurement_rfqs_view": { + "columns": {}, + "definition": "select \"procurement_rfqs\".\"id\" as \"id\", \"procurement_rfqs\".\"rfq_code\" as \"rfq_code\", \"procurement_rfqs\".\"series\" as \"series\", \"procurement_rfqs\".\"rfq_sealed_yn\" as \"rfq_sealed_yn\", \"projects\".\"code\" as \"project_code\", \"projects\".\"name\" as \"project_name\", \"procurement_rfqs\".\"item_code\" as \"item_code\", \"procurement_rfqs\".\"item_name\" as \"item_name\", \"procurement_rfqs\".\"status\" as \"status\", \"procurement_rfqs\".\"pic_code\" as \"pic_code\", \"procurement_rfqs\".\"rfq_send_date\" as \"rfq_send_date\", \"procurement_rfqs\".\"due_date\" as \"due_date\", (\n SELECT MIN(submitted_at)\n FROM procurement_vendor_quotations\n WHERE rfq_id = \"procurement_rfqs\".\"id\"\n AND submitted_at IS NOT NULL\n ) as \"earliest_quotation_submitted_at\", \"created_by_user\".\"name\" as \"created_by_user_name\", \"sent_by_user\".\"name\" as \"sent_by_user_name\", \"procurement_rfqs\".\"updated_at\" as \"updated_at\", \"updated_by_user\".\"name\" as \"updated_by_user_name\", \"procurement_rfqs\".\"remark\" as \"remark\", (\n SELECT material_code \n FROM pr_items \n WHERE procurement_rfqs_id = \"procurement_rfqs\".\"id\"\n AND major_yn = true\n LIMIT 1\n ) as \"major_item_material_code\", (\n SELECT pr_no \n FROM pr_items \n WHERE procurement_rfqs_id = \"procurement_rfqs\".\"id\"\n AND major_yn = true\n LIMIT 1\n ) as \"po_no\", (\n SELECT COUNT(*) \n FROM pr_items \n WHERE procurement_rfqs_id = \"procurement_rfqs\".\"id\"\n ) as \"pr_items_count\" from \"procurement_rfqs\" left join \"projects\" on \"procurement_rfqs\".\"project_id\" = \"projects\".\"id\" left join \"users\" \"created_by_user\" on \"procurement_rfqs\".\"created_by\" = \"created_by_user\".\"id\" left join \"users\" \"updated_by_user\" on \"procurement_rfqs\".\"updated_by\" = \"updated_by_user\".\"id\" left join \"users\" \"sent_by_user\" on \"procurement_rfqs\".\"sent_by\" = \"sent_by_user\".\"id\"", + "name": "procurement_rfqs_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.attachment_revision_history": { + "columns": { + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "attachment_id": { + "name": "attachment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attachment_type": { + "name": "attachment_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "serial_no": { + "name": "serial_no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "client_revision_id": { + "name": "client_revision_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_revision_no": { + "name": "client_revision_no", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "client_file_name": { + "name": "client_file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "client_file_path": { + "name": "client_file_path", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "client_file_size": { + "name": "client_file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_revision_comment": { + "name": "client_revision_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_revision_created_at": { + "name": "client_revision_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_latest_client_revision": { + "name": "is_latest_client_revision", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "total_vendor_responses": { + "name": "total_vendor_responses", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "responded_vendors": { + "name": "responded_vendors", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pending_vendors": { + "name": "pending_vendors", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_response_files": { + "name": "total_response_files", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT \n br.id as rfq_id,\n br.rfq_code,\n ba.id as attachment_id,\n ba.attachment_type,\n ba.serial_no,\n \n -- 발주처 리비전 정보\n rev.id as client_revision_id,\n rev.revision_no as client_revision_no,\n rev.original_file_name as client_file_name,\n rev.file_size as client_file_size,\n rev.file_path as client_file_path,\n rev.revision_comment as client_revision_comment,\n rev.created_at as client_revision_created_at,\n rev.is_latest as is_latest_client_revision,\n \n -- 벤더 응답 통계\n COALESCE(response_stats.total_responses, 0) as total_vendor_responses,\n COALESCE(response_stats.responded_count, 0) as responded_vendors,\n COALESCE(response_stats.pending_count, 0) as pending_vendors,\n COALESCE(response_stats.total_files, 0) as total_response_files\n \n FROM b_rfqs br\n JOIN b_rfq_attachments ba ON br.id = ba.rfq_id\n JOIN b_rfq_attachment_revisions rev ON ba.id = rev.attachment_id\n LEFT JOIN (\n SELECT \n var.attachment_id,\n COUNT(*) as total_responses,\n COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) as responded_count,\n COUNT(CASE WHEN var.response_status = 'NOT_RESPONDED' THEN 1 END) as pending_count,\n COUNT(vra.id) as total_files\n FROM vendor_attachment_responses var\n LEFT JOIN vendor_response_attachments_b vra ON var.id = vra.vendor_response_id\n GROUP BY var.attachment_id\n ) response_stats ON ba.id = response_stats.attachment_id\n \n ORDER BY ba.id, rev.created_at DESC\n", + "name": "attachment_revision_history", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.attachments_with_latest_revision": { + "columns": { + "attachment_id": { + "name": "attachment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attachment_type": { + "name": "attachment_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "serial_no": { + "name": "serial_no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "current_revision": { + "name": "current_revision", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "revision_id": { + "name": "revision_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "original_file_name": { + "name": "original_file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "file_path": { + "name": "file_path", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "revision_comment": { + "name": "revision_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_by_name": { + "name": "created_by_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT \n a.id as attachment_id,\n a.attachment_type,\n a.serial_no,\n a.rfq_id,\n a.description,\n a.current_revision,\n \n r.id as revision_id,\n r.file_name,\n r.original_file_name,\n r.file_path,\n r.file_size,\n r.file_type,\n r.revision_comment,\n \n a.created_by,\n u.name as created_by_name,\n a.created_at,\n a.updated_at\n FROM b_rfq_attachments a\n LEFT JOIN b_rfq_attachment_revisions r ON a.latest_revision_id = r.id\n LEFT JOIN users u ON a.created_by = u.id\n ", + "name": "attachments_with_latest_revision", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.b_rfqs_master": { + "columns": { + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "pic_code": { + "name": "pic_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "pic_name": { + "name": "pic_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "eng_pic_name": { + "name": "eng_pic_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "package_no": { + "name": "package_no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "package_name": { + "name": "package_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "project_code": { + "name": "project_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_type": { + "name": "project_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "project_company": { + "name": "project_company", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_flag": { + "name": "project_flag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_site": { + "name": "project_site", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "total_attachments": { + "name": "total_attachments", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT \n br.id as rfq_id,\n br.rfq_code,\n br.description,\n br.status,\n br.due_date,\n br.pic_code,\n br.pic_name,\n br.eng_pic_name,\n br.package_no,\n br.package_name,\n br.project_id,\n p.code as project_code,\n p.name as project_name,\n p.type as project_type,\n br.project_company,\n br.project_flag,\n br.project_site,\n COALESCE(att_count.total_attachments, 0) as total_attachments,\n br.created_at,\n br.updated_at\n FROM b_rfqs br\n LEFT JOIN projects p ON br.project_id = p.id\n LEFT JOIN (\n SELECT rfq_id, COUNT(*) as total_attachments\n FROM b_rfq_attachments\n GROUP BY rfq_id\n ) att_count ON br.id = att_count.rfq_id\n", + "name": "b_rfqs_master", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.final_rfq_detail": { + "columns": { + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "rfq_status": { + "name": "rfq_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "final_rfq_id": { + "name": "final_rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_rfq_status": { + "name": "final_rfq_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "vendor_country": { + "name": "vendor_country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "vendor_business_size": { + "name": "vendor_business_size", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "valid_date": { + "name": "valid_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "incoterms_code": { + "name": "incoterms_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "incoterms_description": { + "name": "incoterms_description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "payment_terms_code": { + "name": "payment_terms_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "payment_terms_description": { + "name": "payment_terms_description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "tax_code": { + "name": "tax_code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "place_of_shipping": { + "name": "place_of_shipping", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "place_of_destination": { + "name": "place_of_destination", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "short_list": { + "name": "short_list", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "return_yn": { + "name": "return_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cp_request_yn": { + "name": "cp_request_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "prject_gtc_yn": { + "name": "prject_gtc_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "firsttime_yn": { + "name": "firsttime_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "material_price_related_yn": { + "name": "material_price_related_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "return_revision": { + "name": "return_revision", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gtc": { + "name": "gtc", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "gtc_valid_date": { + "name": "gtc_valid_date", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "classification": { + "name": "classification", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "sparepart": { + "name": "sparepart", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor_remark": { + "name": "vendor_remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT \n br.id as rfq_id,\n br.rfq_code,\n br.status as rfq_status,\n fr.id as final_rfq_id,\n fr.final_rfq_status,\n fr.vendor_id,\n v.vendor_code,\n v.vendor_name,\n v.country as vendor_country,\n v.business_size as vendor_business_size,\n fr.due_date,\n fr.valid_date,\n fr.delivery_date,\n fr.incoterms_code,\n inc.description as incoterms_description,\n fr.payment_terms_code,\n pt.description as payment_terms_description,\n fr.currency,\n fr.tax_code,\n fr.place_of_shipping,\n fr.place_of_destination,\n fr.short_list,\n fr.return_yn,\n fr.cp_request_yn,\n fr.prject_gtc_yn,\n fr.firsttime_yn,\n fr.material_price_related_yn,\n fr.return_revision,\n fr.gtc,\n fr.gtc_valid_date,\n fr.classification,\n fr.sparepart,\n fr.remark,\n fr.vendor_remark,\n fr.created_at,\n fr.updated_at\n FROM b_rfqs br\n JOIN final_rfq fr ON br.id = fr.rfq_id\n LEFT JOIN vendors v ON fr.vendor_id = v.id\n LEFT JOIN incoterms inc ON fr.incoterms_code = inc.code\n LEFT JOIN payment_terms pt ON fr.payment_terms_code = pt.code\n", + "name": "final_rfq_detail", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.initial_rfq_detail": { + "columns": { + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "rfq_status": { + "name": "rfq_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "initial_rfq_id": { + "name": "initial_rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "initial_rfq_status": { + "name": "initial_rfq_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "vendor_category": { + "name": "vendor_category", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "vendor_country": { + "name": "vendor_country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "vendor_business_size": { + "name": "vendor_business_size", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "valid_date": { + "name": "valid_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "incoterms_code": { + "name": "incoterms_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "incoterms_description": { + "name": "incoterms_description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "short_list": { + "name": "short_list", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "return_yn": { + "name": "return_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cp_request_yn": { + "name": "cp_request_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "prject_gtc_yn": { + "name": "prject_gtc_yn", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "return_revision": { + "name": "return_revision", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_revision": { + "name": "rfq_revision", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gtc": { + "name": "gtc", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "gtc_valid_date": { + "name": "gtc_valid_date", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "classification": { + "name": "classification", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "sparepart": { + "name": "sparepart", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT \n br.id as rfq_id,\n br.rfq_code,\n br.status as rfq_status,\n ir.id as initial_rfq_id,\n ir.initial_rfq_status,\n ir.vendor_id,\n v.vendor_code,\n v.vendor_name,\n v.country as vendor_country,\n v.business_size as vendor_business_size,\n v.vendor_category as vendor_category,\n ir.due_date,\n ir.valid_date,\n ir.incoterms_code,\n inc.description as incoterms_description,\n ir.short_list,\n ir.return_yn,\n ir.cp_request_yn,\n ir.prject_gtc_yn,\n ir.return_revision,\n ir.rfq_revision,\n ir.gtc,\n ir.gtc_valid_date,\n ir.classification,\n ir.sparepart,\n ir.created_at,\n ir.updated_at\n FROM b_rfqs br\n JOIN initial_rfq ir ON br.id = ir.rfq_id\n LEFT JOIN vendors_with_types v ON ir.vendor_id = v.id\n LEFT JOIN incoterms inc ON ir.incoterms_code = inc.code\n", + "name": "initial_rfq_detail", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.rfq_dashboard": { + "columns": { + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "project_code": { + "name": "project_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_no": { + "name": "package_no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "package_name": { + "name": "package_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "pic_code": { + "name": "pic_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "pic_name": { + "name": "pic_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "eng_pic_name": { + "name": "eng_pic_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "project_company": { + "name": "project_company", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_flag": { + "name": "project_flag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "project_site": { + "name": "project_site", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "total_attachments": { + "name": "total_attachments", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "initial_vendor_count": { + "name": "initial_vendor_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_vendor_count": { + "name": "final_vendor_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "initial_response_rate": { + "name": "initial_response_rate", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "final_response_rate": { + "name": "final_response_rate", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "overall_progress": { + "name": "overall_progress", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "days_to_deadline": { + "name": "days_to_deadline", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_name": { + "name": "updated_by_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "updated_by_email": { + "name": "updated_by_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n -- ② SELECT 절 확장 -------------------------------------------\n SELECT\n br.id AS rfq_id,\n br.rfq_code,\n br.description,\n br.status,\n br.due_date,\n p.code AS project_code,\n p.name AS project_name,\n br.package_no,\n br.package_name,\n br.pic_code,\n br.pic_name,\n br.eng_pic_name,\n br.project_company,\n br.project_flag,\n br.project_site,\n br.remark,\n \n -- 첨부/벤더 요약 -----------------------\n COALESCE(att_count.total_attachments, 0) AS total_attachments,\n COALESCE(init_summary.vendor_count, 0) AS initial_vendor_count,\n COALESCE(final_summary.vendor_count, 0) AS final_vendor_count,\n COALESCE(init_summary.avg_response_rate, 0) AS initial_response_rate,\n COALESCE(final_summary.avg_response_rate, 0) AS final_response_rate,\n \n -- 진행률·마감까지 일수 --------------\n CASE \n WHEN br.status = 'DRAFT' THEN 0\n WHEN br.status = 'Doc. Received' THEN 10\n WHEN br.status = 'PIC Assigned' THEN 20\n WHEN br.status = 'Doc. Confirmed' THEN 30\n WHEN br.status = 'Init. RFQ Sent' THEN 40\n WHEN br.status = 'Init. RFQ Answered' THEN 50\n WHEN br.status = 'TBE started' THEN 60\n WHEN br.status = 'TBE finished' THEN 70\n WHEN br.status = 'Final RFQ Sent' THEN 80\n WHEN br.status = 'Quotation Received' THEN 90\n WHEN br.status = 'Vendor Selected' THEN 100\n ELSE 0\n END AS overall_progress,\n (br.due_date - CURRENT_DATE) AS days_to_deadline,\n \n br.created_at,\n br.updated_at,\n \n -- 💡 추가되는 컬럼 -------------------\n upd.name AS updated_by_name,\n upd.email AS updated_by_email\n FROM b_rfqs br\n LEFT JOIN projects p ON br.project_id = p.id\n \n -- ③ 사용자 정보 조인 --------------------\n LEFT JOIN users upd ON br.updated_by = upd.id\n \n -- (나머지 이미 있던 JOIN 들은 그대로) -----\n LEFT JOIN (\n SELECT rfq_id, COUNT(*) AS total_attachments\n FROM b_rfq_attachments\n GROUP BY rfq_id\n ) att_count ON br.id = att_count.rfq_id\n \n LEFT JOIN (\n SELECT \n rfq_id, \n COUNT(DISTINCT vendor_id) AS vendor_count,\n AVG(response_rate) AS avg_response_rate\n FROM vendor_response_summary\n WHERE rfq_type = 'INITIAL'\n GROUP BY rfq_id\n ) init_summary ON br.id = init_summary.rfq_id\n \n LEFT JOIN (\n SELECT \n rfq_id, \n COUNT(DISTINCT vendor_id) AS vendor_count,\n AVG(response_rate) AS avg_response_rate\n FROM vendor_response_summary\n WHERE rfq_type = 'FINAL'\n GROUP BY rfq_id\n ) final_summary ON br.id = final_summary.rfq_id\n ", + "name": "rfq_dashboard", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.rfq_progress_summary": { + "columns": { + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "rfq_status": { + "name": "rfq_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "days_to_deadline": { + "name": "days_to_deadline", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_attachments": { + "name": "total_attachments", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attachments_with_multiple_revisions": { + "name": "attachments_with_multiple_revisions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_client_revisions": { + "name": "total_client_revisions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "initial_vendor_count": { + "name": "initial_vendor_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "initial_total_responses": { + "name": "initial_total_responses", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "initial_responded_count": { + "name": "initial_responded_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "initial_up_to_date_count": { + "name": "initial_up_to_date_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "initial_version_mismatch_count": { + "name": "initial_version_mismatch_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "initial_response_rate": { + "name": "initial_response_rate", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "initial_version_match_rate": { + "name": "initial_version_match_rate", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "final_vendor_count": { + "name": "final_vendor_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_total_responses": { + "name": "final_total_responses", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_responded_count": { + "name": "final_responded_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_up_to_date_count": { + "name": "final_up_to_date_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_version_mismatch_count": { + "name": "final_version_mismatch_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_response_rate": { + "name": "final_response_rate", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "final_version_match_rate": { + "name": "final_version_match_rate", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "total_response_files": { + "name": "total_response_files", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT \n br.id as rfq_id,\n br.rfq_code,\n br.status as rfq_status,\n br.due_date,\n (br.due_date - CURRENT_DATE) as days_to_deadline,\n \n -- 첨부파일 통계\n attachment_stats.total_attachments,\n attachment_stats.attachments_with_multiple_revisions,\n attachment_stats.total_client_revisions,\n \n -- Initial RFQ 통계\n COALESCE(initial_stats.vendor_count, 0) as initial_vendor_count,\n COALESCE(initial_stats.total_responses, 0) as initial_total_responses,\n COALESCE(initial_stats.responded_count, 0) as initial_responded_count,\n COALESCE(initial_stats.up_to_date_count, 0) as initial_up_to_date_count,\n COALESCE(initial_stats.version_mismatch_count, 0) as initial_version_mismatch_count,\n COALESCE(initial_stats.response_rate, 0) as initial_response_rate,\n COALESCE(initial_stats.version_match_rate, 0) as initial_version_match_rate,\n \n -- Final RFQ 통계\n COALESCE(final_stats.vendor_count, 0) as final_vendor_count,\n COALESCE(final_stats.total_responses, 0) as final_total_responses,\n COALESCE(final_stats.responded_count, 0) as final_responded_count,\n COALESCE(final_stats.up_to_date_count, 0) as final_up_to_date_count,\n COALESCE(final_stats.version_mismatch_count, 0) as final_version_mismatch_count,\n COALESCE(final_stats.response_rate, 0) as final_response_rate,\n COALESCE(final_stats.version_match_rate, 0) as final_version_match_rate,\n \n COALESCE(file_stats.total_files, 0) as total_response_files\n \n FROM b_rfqs br\n LEFT JOIN (\n SELECT \n ba.rfq_id,\n COUNT(*) as total_attachments,\n COUNT(CASE WHEN rev_count.total_revisions > 1 THEN 1 END) as attachments_with_multiple_revisions,\n SUM(rev_count.total_revisions) as total_client_revisions\n FROM b_rfq_attachments ba\n LEFT JOIN (\n SELECT \n attachment_id,\n COUNT(*) as total_revisions\n FROM b_rfq_attachment_revisions\n GROUP BY attachment_id\n ) rev_count ON ba.id = rev_count.attachment_id\n GROUP BY ba.rfq_id\n ) attachment_stats ON br.id = attachment_stats.rfq_id\n \n LEFT JOIN (\n SELECT \n br.id as rfq_id,\n COUNT(DISTINCT var.vendor_id) as vendor_count,\n COUNT(*) as total_responses,\n COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) as responded_count,\n COUNT(CASE WHEN vrd.effective_status = 'UP_TO_DATE' THEN 1 END) as up_to_date_count,\n COUNT(CASE WHEN vrd.effective_status = 'VERSION_MISMATCH' THEN 1 END) as version_mismatch_count,\n ROUND(\n COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) * 100.0 / \n NULLIF(COUNT(*), 0), 2\n ) as response_rate,\n ROUND(\n COUNT(CASE WHEN vrd.effective_status = 'UP_TO_DATE' THEN 1 END) * 100.0 / \n NULLIF(COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END), 0), 2\n ) as version_match_rate\n FROM b_rfqs br\n JOIN vendor_response_detail vrd ON br.id = vrd.rfq_id\n JOIN vendor_attachment_responses var ON vrd.response_id = var.id\n WHERE var.rfq_type = 'INITIAL'\n GROUP BY br.id\n ) initial_stats ON br.id = initial_stats.rfq_id\n \n LEFT JOIN (\n SELECT \n br.id as rfq_id,\n COUNT(DISTINCT var.vendor_id) as vendor_count,\n COUNT(*) as total_responses,\n COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) as responded_count,\n COUNT(CASE WHEN vrd.effective_status = 'UP_TO_DATE' THEN 1 END) as up_to_date_count,\n COUNT(CASE WHEN vrd.effective_status = 'VERSION_MISMATCH' THEN 1 END) as version_mismatch_count,\n ROUND(\n COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) * 100.0 / \n NULLIF(COUNT(*), 0), 2\n ) as response_rate,\n ROUND(\n COUNT(CASE WHEN vrd.effective_status = 'UP_TO_DATE' THEN 1 END) * 100.0 / \n NULLIF(COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END), 0), 2\n ) as version_match_rate\n FROM b_rfqs br\n JOIN vendor_response_detail vrd ON br.id = vrd.rfq_id\n JOIN vendor_attachment_responses var ON vrd.response_id = var.id\n WHERE var.rfq_type = 'FINAL'\n GROUP BY br.id\n ) final_stats ON br.id = final_stats.rfq_id\n \n LEFT JOIN (\n SELECT \n br.id as rfq_id,\n COUNT(vra.id) as total_files\n FROM b_rfqs br\n JOIN b_rfq_attachments ba ON br.id = ba.rfq_id\n JOIN vendor_attachment_responses var ON ba.id = var.attachment_id\n LEFT JOIN vendor_response_attachments_b vra ON var.id = vra.vendor_response_id\n GROUP BY br.id\n ) file_stats ON br.id = file_stats.rfq_id\n", + "name": "rfq_progress_summary", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_response_attachments_enhanced": { + "columns": { + "response_attachment_id": { + "name": "response_attachment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_response_id": { + "name": "vendor_response_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "original_file_name": { + "name": "original_file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "file_path": { + "name": "file_path", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attachment_id": { + "name": "attachment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_type": { + "name": "rfq_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "rfq_record_id": { + "name": "rfq_record_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "current_revision": { + "name": "current_revision", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "responded_revision": { + "name": "responded_revision", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "response_comment": { + "name": "response_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor_comment": { + "name": "vendor_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision_request_comment": { + "name": "revision_request_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "responded_at": { + "name": "responded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revision_requested_at": { + "name": "revision_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attachment_type": { + "name": "attachment_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "serial_no": { + "name": "serial_no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "vendor_country": { + "name": "vendor_country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "latest_client_revision_id": { + "name": "latest_client_revision_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latest_client_revision_no": { + "name": "latest_client_revision_no", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "latest_client_file_name": { + "name": "latest_client_file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "is_version_matched": { + "name": "is_version_matched", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "version_lag": { + "name": "version_lag", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "needs_update": { + "name": "needs_update", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "file_sequence": { + "name": "file_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_latest_response_file": { + "name": "is_latest_response_file", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT \n vra.id as response_attachment_id,\n vra.vendor_response_id,\n vra.file_name,\n vra.original_file_name,\n vra.file_path,\n vra.file_size,\n vra.file_type,\n vra.description,\n vra.uploaded_at,\n \n -- 응답 기본 정보\n var.attachment_id,\n var.vendor_id,\n var.rfq_type,\n var.rfq_record_id,\n var.response_status,\n var.current_revision,\n var.responded_revision,\n \n -- 코멘트 (새로 추가된 필드 포함)\n var.response_comment,\n var.vendor_comment,\n var.revision_request_comment,\n \n -- 날짜 (새로 추가된 필드 포함)\n var.requested_at,\n var.responded_at,\n var.revision_requested_at,\n \n -- 첨부파일 정보\n ba.attachment_type,\n ba.serial_no,\n ba.rfq_id,\n \n -- 벤더 정보\n v.vendor_code,\n v.vendor_name,\n v.country as vendor_country,\n \n -- 발주처 현재 리비전 정보\n latest_rev.id as latest_client_revision_id,\n latest_rev.revision_no as latest_client_revision_no,\n latest_rev.original_file_name as latest_client_file_name,\n \n -- 리비전 비교\n CASE \n WHEN var.responded_revision = ba.current_revision THEN true \n ELSE false \n END as is_version_matched,\n \n -- 버전 차이 계산 (Rev.0, Rev.1 형태 가정)\n CASE \n WHEN var.responded_revision IS NULL THEN NULL\n WHEN ba.current_revision IS NULL THEN NULL\n ELSE CAST(SUBSTRING(ba.current_revision FROM '[0-9]+') AS INTEGER) - \n CAST(SUBSTRING(var.responded_revision FROM '[0-9]+') AS INTEGER)\n END as version_lag,\n \n CASE \n WHEN var.response_status = 'RESPONDED' \n AND var.responded_revision != ba.current_revision THEN true \n ELSE false \n END as needs_update,\n \n -- 파일 순서\n ROW_NUMBER() OVER (\n PARTITION BY var.id \n ORDER BY vra.uploaded_at DESC\n ) as file_sequence,\n \n -- 최신 응답 파일 여부\n CASE \n WHEN ROW_NUMBER() OVER (\n PARTITION BY var.id \n ORDER BY vra.uploaded_at DESC\n ) = 1 THEN true \n ELSE false \n END as is_latest_response_file\n \n FROM vendor_response_attachments_b vra\n JOIN vendor_attachment_responses var ON vra.vendor_response_id = var.id\n JOIN b_rfq_attachments ba ON var.attachment_id = ba.id\n LEFT JOIN vendors v ON var.vendor_id = v.id\n LEFT JOIN b_rfq_attachment_revisions latest_rev ON ba.latest_revision_id = latest_rev.id\n", + "name": "vendor_response_attachments_enhanced", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_response_detail": { + "columns": { + "response_id": { + "name": "response_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "rfq_type": { + "name": "rfq_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "rfq_record_id": { + "name": "rfq_record_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attachment_id": { + "name": "attachment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attachment_type": { + "name": "attachment_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "serial_no": { + "name": "serial_no", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "attachment_description": { + "name": "attachment_description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "vendor_country": { + "name": "vendor_country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "current_revision": { + "name": "current_revision", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "responded_revision": { + "name": "responded_revision", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "response_comment": { + "name": "response_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor_comment": { + "name": "vendor_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision_request_comment": { + "name": "revision_request_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "responded_at": { + "name": "responded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revision_requested_at": { + "name": "revision_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "latest_client_revision_no": { + "name": "latest_client_revision_no", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "latest_client_file_name": { + "name": "latest_client_file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "latest_client_file_size": { + "name": "latest_client_file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latest_client_revision_comment": { + "name": "latest_client_revision_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_version_matched": { + "name": "is_version_matched", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "version_lag": { + "name": "version_lag", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "needs_update": { + "name": "needs_update", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_multiple_revisions": { + "name": "has_multiple_revisions", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "total_response_files": { + "name": "total_response_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latest_response_file_name": { + "name": "latest_response_file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "latest_response_file_size": { + "name": "latest_response_file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latest_response_uploaded_at": { + "name": "latest_response_uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "effective_status": { + "name": "effective_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT \n var.id as response_id,\n ba.rfq_id,\n br.rfq_code,\n var.rfq_type,\n var.rfq_record_id,\n \n -- 첨부파일 정보\n ba.id as attachment_id,\n ba.attachment_type,\n ba.serial_no,\n ba.description as attachment_description,\n \n -- 벤더 정보\n v.id as vendor_id,\n v.vendor_code,\n v.vendor_name,\n v.country as vendor_country,\n \n -- 응답 상태\n var.response_status,\n var.current_revision,\n var.responded_revision,\n \n -- 코멘트 (새로 추가된 필드 포함)\n var.response_comment,\n var.vendor_comment,\n var.revision_request_comment,\n \n -- 날짜 (새로 추가된 필드 포함)\n var.requested_at,\n var.responded_at,\n var.revision_requested_at,\n \n -- 발주처 최신 리비전\n latest_rev.revision_no as latest_client_revision_no,\n latest_rev.original_file_name as latest_client_file_name,\n latest_rev.file_size as latest_client_file_size,\n latest_rev.revision_comment as latest_client_revision_comment,\n \n -- 리비전 분석\n CASE \n WHEN var.responded_revision = ba.current_revision THEN true \n ELSE false \n END as is_version_matched,\n \n CASE \n WHEN var.responded_revision IS NULL OR ba.current_revision IS NULL THEN NULL\n ELSE CAST(SUBSTRING(ba.current_revision FROM '[0-9]+') AS INTEGER) - \n CAST(SUBSTRING(var.responded_revision FROM '[0-9]+') AS INTEGER)\n END as version_lag,\n \n CASE \n WHEN var.response_status = 'RESPONDED' \n AND var.responded_revision != ba.current_revision THEN true \n ELSE false \n END as needs_update,\n \n CASE \n WHEN revision_count.total_revisions > 1 THEN true \n ELSE false \n END as has_multiple_revisions,\n \n -- 응답 파일 정보\n COALESCE(file_stats.total_files, 0) as total_response_files,\n file_stats.latest_file_name as latest_response_file_name,\n file_stats.latest_file_size as latest_response_file_size,\n file_stats.latest_uploaded_at as latest_response_uploaded_at,\n \n -- 효과적인 상태\n CASE \n WHEN var.response_status = 'NOT_RESPONDED' THEN 'NOT_RESPONDED'\n WHEN var.response_status = 'WAIVED' THEN 'WAIVED'\n WHEN var.response_status = 'REVISION_REQUESTED' THEN 'REVISION_REQUESTED'\n WHEN var.response_status = 'RESPONDED' AND var.responded_revision = ba.current_revision THEN 'UP_TO_DATE'\n WHEN var.response_status = 'RESPONDED' AND var.responded_revision != ba.current_revision THEN 'VERSION_MISMATCH'\n ELSE var.response_status\n END as effective_status\n \n FROM vendor_attachment_responses var\n JOIN b_rfq_attachments ba ON var.attachment_id = ba.id\n JOIN b_rfqs br ON ba.rfq_id = br.id\n LEFT JOIN vendors v ON var.vendor_id = v.id\n LEFT JOIN b_rfq_attachment_revisions latest_rev ON ba.latest_revision_id = latest_rev.id\n LEFT JOIN (\n SELECT \n attachment_id,\n COUNT(*) as total_revisions\n FROM b_rfq_attachment_revisions\n GROUP BY attachment_id\n ) revision_count ON ba.id = revision_count.attachment_id\n LEFT JOIN (\n SELECT \n vendor_response_id,\n COUNT(*) as total_files,\n MAX(original_file_name) as latest_file_name,\n MAX(file_size) as latest_file_size,\n MAX(uploaded_at) as latest_uploaded_at\n FROM vendor_response_attachments_b\n GROUP BY vendor_response_id\n ) file_stats ON var.id = file_stats.vendor_response_id\n", + "name": "vendor_response_detail", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.vendor_response_summary": { + "columns": { + "rfq_id": { + "name": "rfq_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rfq_code": { + "name": "rfq_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "rfq_status": { + "name": "rfq_status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "vendor_country": { + "name": "vendor_country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "vendor_business_size": { + "name": "vendor_business_size", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "rfq_type": { + "name": "rfq_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "total_attachments": { + "name": "total_attachments", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "responded_count": { + "name": "responded_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pending_count": { + "name": "pending_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "waived_count": { + "name": "waived_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "revision_requested_count": { + "name": "revision_requested_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_rate": { + "name": "response_rate", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "completion_rate": { + "name": "completion_rate", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT \n br.id as rfq_id,\n br.rfq_code,\n br.status as rfq_status,\n v.id as vendor_id,\n v.vendor_code,\n v.vendor_name,\n v.country as vendor_country,\n v.business_size as vendor_business_size,\n var.rfq_type,\n COUNT(var.id) as total_attachments,\n COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) as responded_count,\n COUNT(CASE WHEN var.response_status = 'NOT_RESPONDED' THEN 1 END) as pending_count,\n COUNT(CASE WHEN var.response_status = 'WAIVED' THEN 1 END) as waived_count,\n COUNT(CASE WHEN var.response_status = 'REVISION_REQUESTED' THEN 1 END) as revision_requested_count,\n ROUND(\n (COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) * 100.0 / \n NULLIF(COUNT(CASE WHEN var.response_status != 'WAIVED' THEN 1 END), 0)), \n 2\n ) as response_rate,\n ROUND(\n ((COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) + \n COUNT(CASE WHEN var.response_status = 'WAIVED' THEN 1 END)) * 100.0 / COUNT(var.id)), \n 2\n ) as completion_rate\n FROM b_rfqs br\n JOIN b_rfq_attachments bra ON br.id = bra.rfq_id\n JOIN vendor_attachment_responses var ON bra.id = var.attachment_id\n JOIN vendors v ON var.vendor_id = v.id\n GROUP BY br.id, br.rfq_code, br.status, v.id, v.vendor_code, v.vendor_name, v.country, v.business_size, var.rfq_type\n", + "name": "vendor_response_summary", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.tech_vendor_candidates_with_vendor_info": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "company_name": { + "name": "company_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "contact_email": { + "name": "contact_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "contact_phone": { + "name": "contact_phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'COLLECTED'" + }, + "items": { + "name": "items", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"tech_vendor_candidates\".\"id\", \"tech_vendor_candidates\".\"company_name\", \"tech_vendor_candidates\".\"contact_email\", \"tech_vendor_candidates\".\"contact_phone\", \"tech_vendor_candidates\".\"tax_id\", \"tech_vendor_candidates\".\"address\", \"tech_vendor_candidates\".\"country\", \"tech_vendor_candidates\".\"source\", \"tech_vendor_candidates\".\"status\", \"tech_vendor_candidates\".\"items\", \"tech_vendor_candidates\".\"remark\", \"tech_vendor_candidates\".\"created_at\", \"tech_vendor_candidates\".\"updated_at\", \"tech_vendors\".\"vendor_name\", \"tech_vendors\".\"vendor_code\", \"tech_vendors\".\"created_at\" as \"vendor_created_at\" from \"tech_vendor_candidates\" left join \"tech_vendors\" on \"tech_vendor_candidates\".\"vendor_id\" = \"tech_vendors\".\"id\"", + "name": "tech_vendor_candidates_with_vendor_info", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.tech_vendor_detail_view": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country_eng": { + "name": "country_eng", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country_fab": { + "name": "country_fab", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "agent_phone": { + "name": "agent_phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "agent_email": { + "name": "agent_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "tech_vendor_type": { + "name": "tech_vendor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "representative_name": { + "name": "representative_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "representative_email": { + "name": "representative_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "representative_phone": { + "name": "representative_phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "representative_birth": { + "name": "representative_birth", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "definition": "select \"id\", \"vendor_name\", \"vendor_code\", \"tax_id\", \"address\", \"country\", \"country_eng\", \"country_fab\", \"agent_name\", \"agent_phone\", \"agent_email\", \"phone\", \"email\", \"website\", \"status\", \"tech_vendor_type\", \"representative_name\", \"representative_email\", \"representative_phone\", \"representative_birth\", \"created_at\", \"updated_at\", \n (SELECT COALESCE(\n json_agg(\n json_build_object(\n 'id', c.id,\n 'contactName', c.contact_name,\n 'contactPosition', c.contact_position,\n 'contactEmail', c.contact_email,\n 'contactPhone', c.contact_phone,\n 'isPrimary', c.is_primary\n )\n ),\n '[]'::json\n )\n FROM vendor_contacts c\n WHERE c.vendor_id = tech_vendors.id)\n as \"contacts\", \n (SELECT COALESCE(\n json_agg(\n json_build_object(\n 'id', a.id,\n 'fileName', a.file_name,\n 'filePath', a.file_path,\n 'attachmentType', a.attachment_type,\n 'createdAt', a.created_at\n )\n ORDER BY a.attachment_type, a.created_at DESC\n ),\n '[]'::json\n )\n FROM tech_vendor_attachments a\n WHERE a.vendor_id = tech_vendors.id)\n as \"attachments\", \n (SELECT COUNT(*)\n FROM tech_vendor_attachments a\n WHERE a.vendor_id = tech_vendors.id)\n as \"attachment_count\", \n (SELECT COUNT(*) \n FROM vendor_contacts c\n WHERE c.vendor_id = tech_vendors.id)\n as \"contact_count\", \n (SELECT COALESCE(\n json_agg(\n json_build_object(\n 'itemCode', i.item_code,\n 'itemName', it.item_name\n )\n ),\n '[]'::json\n )\n FROM tech_vendor_possible_items i\n LEFT JOIN items it ON i.item_code = it.item_code\n WHERE i.vendor_id = tech_vendors.id)\n as \"possible_items\", \n (SELECT COUNT(*) \n FROM tech_vendor_possible_items i\n WHERE i.vendor_id = tech_vendors.id)\n as \"item_count\" from \"tech_vendors\"", + "name": "tech_vendor_detail_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.tech_vendor_items_view": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_code": { + "name": "item_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "item_name": { + "name": "item_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "definition": "select \"tech_vendor_possible_items\".\"id\", \"tech_vendor_possible_items\".\"vendor_id\", \"items\".\"item_code\", \"items\".\"item_name\", \"tech_vendor_possible_items\".\"created_at\", \"tech_vendor_possible_items\".\"updated_at\" from \"tech_vendor_possible_items\" left join \"items\" on \"tech_vendor_possible_items\".\"item_code\" = \"items\".\"item_code\"", + "name": "tech_vendor_items_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.esg_evaluations_view": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "serial_number": { + "name": "serial_number", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "inspection_item": { + "name": "inspection_item", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "definition": "select \"esg_evaluations\".\"id\", \"esg_evaluations\".\"serial_number\", \"esg_evaluations\".\"category\", \"esg_evaluations\".\"inspection_item\", \"esg_evaluations\".\"is_active\", \"esg_evaluations\".\"created_at\", \"esg_evaluations\".\"updated_at\", count(distinct \"esg_evaluation_items\".\"id\") as \"total_evaluation_items\", count(\"esg_answer_options\".\"id\") as \"total_answer_options\", coalesce(sum(\"esg_answer_options\".\"score\"), 0) as \"max_possible_score\", \n (\n SELECT array_agg(evaluation_item order by order_index) \n FROM esg_evaluation_items \n WHERE esg_evaluation_id = \"esg_evaluations\".\"id\" \n AND is_active = true \n AND evaluation_item is not null\n )\n as \"evaluation_items_list\" from \"esg_evaluations\" left join \"esg_evaluation_items\" on \"esg_evaluations\".\"id\" = \"esg_evaluation_items\".\"esg_evaluation_id\" AND \"esg_evaluation_items\".\"is_active\" = true left join \"esg_answer_options\" on \"esg_evaluation_items\".\"id\" = \"esg_answer_options\".\"esg_evaluation_item_id\" AND \"esg_answer_options\".\"is_active\" = true group by \"esg_evaluations\".\"id\", \"esg_evaluations\".\"serial_number\", \"esg_evaluations\".\"category\", \"esg_evaluations\".\"inspection_item\", \"esg_evaluations\".\"is_active\", \"esg_evaluations\".\"created_at\", \"esg_evaluations\".\"updated_at\"", + "name": "esg_evaluations_view", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.evaluation_targets_with_departments": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "evaluation_year": { + "name": "evaluation_year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "division": { + "name": "division", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "vendor_code": { + "name": "vendor_code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "vendor_name": { + "name": "vendor_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "domestic_foreign": { + "name": "domestic_foreign", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "material_type": { + "name": "material_type", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "consensus_status": { + "name": "consensus_status", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "admin_comment": { + "name": "admin_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consolidated_comment": { + "name": "consolidated_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "confirmed_by": { + "name": "confirmed_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ld_claim_count": { + "name": "ld_claim_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "ld_claim_amount": { + "name": "ld_claim_amount", + "type": "numeric(15, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "ld_claim_currency": { + "name": "ld_claim_currency", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'KRW'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "definition": "select \"evaluation_targets\".\"id\", \"evaluation_targets\".\"evaluation_year\", \"evaluation_targets\".\"division\", \"evaluation_targets\".\"vendor_code\", \"evaluation_targets\".\"vendor_name\", \"evaluation_targets\".\"domestic_foreign\", \"evaluation_targets\".\"material_type\", \"evaluation_targets\".\"status\", \"evaluation_targets\".\"consensus_status\", \"evaluation_targets\".\"admin_comment\", \"evaluation_targets\".\"consolidated_comment\", \"evaluation_targets\".\"confirmed_at\", \"evaluation_targets\".\"confirmed_by\", \"evaluation_targets\".\"ld_claim_count\", \"evaluation_targets\".\"ld_claim_amount\", \"evaluation_targets\".\"ld_claim_currency\", \"evaluation_targets\".\"created_at\", \"evaluation_targets\".\"updated_at\", order_reviewer.name as \"order_reviewer_name\", order_reviewer.email as \"order_reviewer_email\", order_etr.department_name_from as \"order_department_name\", order_review.is_approved as \"order_is_approved\", order_review.reviewed_at as \"order_reviewed_at\", procurement_reviewer.name as \"procurement_reviewer_name\", procurement_reviewer.email as \"procurement_reviewer_email\", procurement_etr.department_name_from as \"procurement_department_name\", procurement_review.is_approved as \"procurement_is_approved\", procurement_review.reviewed_at as \"procurement_reviewed_at\", quality_reviewer.name as \"quality_reviewer_name\", quality_reviewer.email as \"quality_reviewer_email\", quality_etr.department_name_from as \"quality_department_name\", quality_review.is_approved as \"quality_is_approved\", quality_review.reviewed_at as \"quality_reviewed_at\", design_reviewer.name as \"design_reviewer_name\", design_reviewer.email as \"design_reviewer_email\", design_etr.department_name_from as \"design_department_name\", design_review.is_approved as \"design_is_approved\", design_review.reviewed_at as \"design_reviewed_at\", cs_reviewer.name as \"cs_reviewer_name\", cs_reviewer.email as \"cs_reviewer_email\", cs_etr.department_name_from as \"cs_department_name\", cs_review.is_approved as \"cs_is_approved\", cs_review.reviewed_at as \"cs_reviewed_at\" from \"evaluation_targets\" left join evaluation_target_reviewers order_etr on \"evaluation_targets\".\"id\" = order_etr.evaluation_target_id AND order_etr.department_code = 'ORDER_EVAL' left join users order_reviewer on order_etr.reviewer_user_id = order_reviewer.id left join evaluation_target_reviews order_review on \"evaluation_targets\".\"id\" = order_review.evaluation_target_id AND order_review.reviewer_user_id = order_reviewer.id left join evaluation_target_reviewers procurement_etr on \"evaluation_targets\".\"id\" = procurement_etr.evaluation_target_id AND procurement_etr.department_code = 'PROCUREMENT_EVAL' left join users procurement_reviewer on procurement_etr.reviewer_user_id = procurement_reviewer.id left join evaluation_target_reviews procurement_review on \"evaluation_targets\".\"id\" = procurement_review.evaluation_target_id AND procurement_review.reviewer_user_id = procurement_reviewer.id left join evaluation_target_reviewers quality_etr on \"evaluation_targets\".\"id\" = quality_etr.evaluation_target_id AND quality_etr.department_code = 'QUALITY_EVAL' left join users quality_reviewer on quality_etr.reviewer_user_id = quality_reviewer.id left join evaluation_target_reviews quality_review on \"evaluation_targets\".\"id\" = quality_review.evaluation_target_id AND quality_review.reviewer_user_id = quality_reviewer.id left join evaluation_target_reviewers design_etr on \"evaluation_targets\".\"id\" = design_etr.evaluation_target_id AND design_etr.department_code = 'DESIGN_EVAL' left join users design_reviewer on design_etr.reviewer_user_id = design_reviewer.id left join evaluation_target_reviews design_review on \"evaluation_targets\".\"id\" = design_review.evaluation_target_id AND design_review.reviewer_user_id = design_reviewer.id left join evaluation_target_reviewers cs_etr on \"evaluation_targets\".\"id\" = cs_etr.evaluation_target_id AND cs_etr.department_code = 'CS_EVAL' left join users cs_reviewer on cs_etr.reviewer_user_id = cs_reviewer.id left join evaluation_target_reviews cs_review on \"evaluation_targets\".\"id\" = cs_review.evaluation_target_id AND cs_review.reviewer_user_id = cs_reviewer.id", + "name": "evaluation_targets_with_departments", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.project_gtc_view": { + "columns": {}, + "definition": "select \"projects\".\"id\" as \"id\", \"projects\".\"code\" as \"code\", \"projects\".\"name\" as \"name\", \"projects\".\"type\" as \"type\", \"projects\".\"created_at\" as \"project_created_at\", \"projects\".\"updated_at\" as \"project_updated_at\", \"project_gtc_files\".\"id\" as \"gtc_file_id\", \"project_gtc_files\".\"file_name\" as \"fileName\", \"project_gtc_files\".\"file_path\" as \"filePath\", \"project_gtc_files\".\"original_file_name\" as \"originalFileName\", \"project_gtc_files\".\"file_size\" as \"fileSize\", \"project_gtc_files\".\"mime_type\" as \"mimeType\", \"project_gtc_files\".\"created_at\" as \"gtcCreatedAt\", \"project_gtc_files\".\"updated_at\" as \"gtcUpdatedAt\" from \"projects\" left join \"project_gtc_files\" on \"projects\".\"id\" = \"project_gtc_files\".\"project_id\"", + "name": "project_gtc_view", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +}
\ No newline at end of file diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index 70b29664..8d2737ff 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -1100,6 +1100,13 @@ "when": 1750310865435, "tag": "0156_premium_tyger_tiger", "breakpoints": true + }, + { + "idx": 157, + "version": "7", + "when": 1750379942192, + "tag": "0157_oval_sunfire", + "breakpoints": true } ] }
\ No newline at end of file diff --git a/db/schema/evaluationCriteria.ts b/db/schema/evaluationCriteria.ts new file mode 100644 index 00000000..23a987cf --- /dev/null +++ b/db/schema/evaluationCriteria.ts @@ -0,0 +1,137 @@ +/* IMPORT */
+import {
+ decimal,
+ integer,
+ pgTable,
+ pgView,
+ serial,
+ text,
+ timestamp,
+ varchar,
+} from 'drizzle-orm/pg-core';
+import { eq, relations } from 'drizzle-orm';
+
+// ----------------------------------------------------------------------------------------------------
+
+/* CONSTANTS */
+const REG_EVAL_CRITERIA_CATEGORY = [
+ { label: 'CS', value: 'customer-service' },
+ { label: '관리자', value: 'administrator' },
+ { label: '구매', value: 'procurement' },
+ { label: '설계', value: 'design' },
+ { label: '조달', value: 'sourcing' },
+ { label: '품질', value: 'quality' },
+];
+const REG_EVAL_CRITERIA_ITEM = [
+ { label: '가점항목', value: 'customer-service' },
+ { label: '납기', value: 'delivery' },
+ { label: '경영현황', value: 'management-status' },
+ { label: '감점항목', value: 'penalty-item' },
+ { label: '구매', value: 'procurement' },
+ { label: '품질', value: 'quality' },
+];
+const REG_EVAL_CRITERIA_CATEGORY2 = [
+ { label: '공정', value: 'processScore' },
+ { label: '가격', value: 'priceScore' },
+ { label: '납기', value: 'deliveryScore' },
+ { label: '자율평가', value: 'selfEvaluationScore' },
+];
+
+const REG_EVAL_CRITERIA_CATEGORY_ENUM = REG_EVAL_CRITERIA_CATEGORY.map(c => c.value) as [string, ...string[]];
+const REG_EVAL_CRITERIA_CATEGORY_ENUM2 = REG_EVAL_CRITERIA_CATEGORY2.map(c => c.value) as [string, ...string[]];
+const REG_EVAL_CRITERIA_ITEM_ENUM = REG_EVAL_CRITERIA_ITEM.map(c => c.value) as [string, ...string[]];
+
+// ----------------------------------------------------------------------------------------------------
+
+/* TABLE SCHEMATA */
+const regEvalCriteria = pgTable('reg_eval_criteria', {
+ id: serial('id').primaryKey(),
+ category: varchar('category', { enum: REG_EVAL_CRITERIA_CATEGORY_ENUM, length: 32 }).default('quality').notNull(),
+ category2: varchar('category', { enum: REG_EVAL_CRITERIA_CATEGORY_ENUM2, length: 32 }).default('processScore').notNull(),
+ item: varchar('item', { enum: REG_EVAL_CRITERIA_ITEM_ENUM, length: 32 }).default('quality').notNull(),
+ classification: varchar('classification', { length: 255 }).notNull(),
+ range: varchar('range', { length: 255 }),
+ remarks: text('remarks'),
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+const regEvalCriteriaDetails = pgTable('reg_eval_criteria_details', {
+ id: serial('id').primaryKey(),
+ criteriaId: integer('criteria_id')
+ .notNull()
+ .references(() => regEvalCriteria.id, { onDelete: 'cascade' }),
+ detail: text('detail').notNull(),
+ orderIndex: integer('order_index').default(0).notNull(),
+ scoreEquipShip: decimal('score_equip_ship', { precision: 5, scale: 2 }),
+ scoreEquipMarine: decimal('score_equip_marine', { precision: 5, scale: 2 }),
+ scoreBulkShip: decimal('score_bulk_ship', { precision: 5, scale: 2 }),
+ scoreBulkMarine: decimal('score_bulk_marine', { precision: 5, scale: 2 }),
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+
+// ----------------------------------------------------------------------------------------------------
+
+/* VIEWS */
+const regEvalCriteriaView = pgView('reg_eval_criteria_view').as((qb) =>
+ qb
+ .select({
+ id: regEvalCriteria.id,
+ category: regEvalCriteria.category,
+ item: regEvalCriteria.item,
+ classification: regEvalCriteria.classification,
+ range: regEvalCriteria.range,
+ detailId: regEvalCriteriaDetails.id,
+ detail: regEvalCriteriaDetails.detail,
+ orderIndex: regEvalCriteriaDetails.orderIndex,
+ scoreEquipShip: regEvalCriteriaDetails.scoreEquipShip,
+ scoreEquipMarine: regEvalCriteriaDetails.scoreEquipMarine,
+ scoreBulkShip: regEvalCriteriaDetails.scoreBulkShip,
+ scoreBulkMarine: regEvalCriteriaDetails.scoreBulkMarine,
+ })
+ .from(regEvalCriteria)
+ .leftJoin(regEvalCriteriaDetails, eq(regEvalCriteria.id, regEvalCriteriaDetails.criteriaId))
+ .orderBy(regEvalCriteria.id, regEvalCriteriaDetails.orderIndex)
+);
+
+// ----------------------------------------------------------------------------------------------------
+
+/* RELATIONS */
+const regEvalCriteriaRelations = relations(regEvalCriteria, ({ many }) => ({
+ details: many(regEvalCriteriaDetails),
+}));
+const regEvalCriteriaDetailsRelations = relations(regEvalCriteriaDetails, ({one}) => ({
+ criteria: one(regEvalCriteria, {
+ fields: [regEvalCriteriaDetails.criteriaId],
+ references: [regEvalCriteria.id],
+ }),
+}));
+
+// ----------------------------------------------------------------------------------------------------
+
+/* TYPES */
+type RegEvalCriteria = typeof regEvalCriteria.$inferSelect;
+type NewRegEvalCriteria = typeof regEvalCriteria.$inferInsert;
+type RegEvalCriteriaDetails = typeof regEvalCriteriaDetails.$inferSelect;
+type NewRegEvalCriteriaDetails = typeof regEvalCriteriaDetails.$inferInsert;
+type RegEvalCriteriaWithDetails = RegEvalCriteria & { criteriaDetails: RegEvalCriteriaDetails[] };
+type RegEvalCriteriaView = typeof regEvalCriteriaView.$inferSelect;
+
+// ----------------------------------------------------------------------------------------------------
+
+/* Export */
+export {
+ REG_EVAL_CRITERIA_CATEGORY,
+ REG_EVAL_CRITERIA_ITEM,
+ regEvalCriteria,
+ regEvalCriteriaDetails,
+ regEvalCriteriaDetailsRelations,
+ regEvalCriteriaRelations,
+ regEvalCriteriaView,
+ type NewRegEvalCriteria,
+ type NewRegEvalCriteriaDetails,
+ type RegEvalCriteriaView,
+ type RegEvalCriteriaWithDetails,
+ type RegEvalCriteria,
+ type RegEvalCriteriaDetails,
+};
\ No newline at end of file diff --git a/db/schema/evaluationTarget.ts b/db/schema/evaluationTarget.ts index 4eff1c19..915641c8 100644 --- a/db/schema/evaluationTarget.ts +++ b/db/schema/evaluationTarget.ts @@ -3,6 +3,7 @@ import { eq , sql, relations} from "drizzle-orm"; import { vendors } from "./vendors"; import { users } from "./users"; import { contracts } from "./contract"; +import { regEvalCriteriaDetails } from "./evaluationCriteria"; // 평가 대상 메인 테이블 export const evaluationTargets = pgTable("evaluation_targets", { @@ -178,7 +179,7 @@ export const orderRecordsRelations = relations(contracts, ({ one }) => ({ }), })); -// 평가 담당 부서 코드 상수 (조직 API와 매핑) +// 평가 담당 부서 코드 상수 export const EVALUATION_DEPARTMENT_CODES = { ORDER_EVAL: "ORDER_EVAL", // 발주 평가 담당 PROCUREMENT_EVAL: "PROCUREMENT_EVAL", // 조달 평가 담당 @@ -192,7 +193,7 @@ export type EvaluationDepartmentCode = keyof typeof EVALUATION_DEPARTMENT_CODES; // ============= TypeScript 타입 정의 ============= export type EvaluationTargetStatus = "PENDING" | "CONFIRMED" | "EXCLUDED"; - export type Division = "OCEAN" | "SHIPYARD"; + export type Division = "PLANT" | "SHIP"; export type MaterialType = "EQUIPMENT" | "BULK" | "EQUIPMENT_BULK"; export type DomesticForeign = "DOMESTIC" | "FOREIGN"; @@ -383,3 +384,339 @@ export const evaluationTargetsWithDepartments = pgView("evaluation_targets_with_ // 타입 정의 export type EvaluationTargetWithDepartments = typeof evaluationTargetsWithDepartments.$inferSelect; + + +export const periodicEvaluations = pgTable("periodic_evaluations", { + id: serial("id").primaryKey(), + + // 평가 대상 참조 + evaluationTargetId: integer("evaluation_target_id") + .references(() => evaluationTargets.id, { onDelete: "cascade" }) + .notNull(), + + // 평가 기본 정보 + evaluationPeriod: varchar("evaluation_period", { length: 20 }).notNull(), // "상반기", "하반기", "연간" 등 + + // 업체 제출 관련 + documentsSubmitted: boolean("documents_submitted").default(false), + submissionDate: timestamp("submission_date"), + submissionDeadline: timestamp("submission_deadline"), + + // 평가 점수 (최종 확정) + finalScore: decimal("final_score", { precision: 5, scale: 2 }), + finalGrade: varchar("final_grade", { + length: 5, + enum: ["S", "A", "B", "C", "D"] + }), + + // 평가 점수 (평가자 평균) + evaluationScore: decimal("evaluation_score", { precision: 5, scale: 2 }), + evaluationGrade: varchar("evaluation_grade", { + length: 5, + enum: ["S", "A", "B", "C", "D"] + }), + + // 평가항목별 점수 + processScore: decimal("process_score", { precision: 5, scale: 2 }).default("0"), // 공정 + priceScore: decimal("price_score", { precision: 5, scale: 2 }).default("0"), // 가격 + deliveryScore: decimal("delivery_score", { precision: 5, scale: 2 }).default("0"), // 납기 + selfEvaluationScore: decimal("self_evaluation_score", { precision: 5, scale: 2 }).default("0"), // 자율평가 + + // 합계 점수 + totalScore: decimal("total_score", { precision: 5, scale: 2 }).default("0"), + + // 가점/감점 + participationBonus: decimal("participation_bonus", { precision: 5, scale: 2 }).default("0"), // 참여도 가점 + qualityDeduction: decimal("quality_deduction", { precision: 5, scale: 2 }).default("0"), // 품질 감점 + + // 평가 상태 + status: varchar("status", { + length: 30, + enum: ["PENDING_SUBMISSION", "SUBMITTED", "IN_REVIEW", "REVIEW_COMPLETED", "FINALIZED"] + }).notNull().default("PENDING_SUBMISSION"), + + // 평가 완료 정보 + reviewCompletedAt: timestamp("review_completed_at"), + finalizedAt: timestamp("finalized_at"), + finalizedBy: integer("finalized_by").references(() => users.id), + + // 비고 + evaluationNote: text("evaluation_note"), + + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}, (table) => ({ + // 같은 평가대상에 같은 기간에는 하나의 평가만 + uniqueEvaluationTarget: unique("unique_evaluation_target") + .on(table.evaluationTargetId, table.evaluationPeriod), +})); + + +// 2. 리뷰어별 개별 평가 테이블 +export const reviewerEvaluations = pgTable("reviewer_evaluations", { + id: serial("id").primaryKey(), + + // 정기평가 참조 + periodicEvaluationId: integer("periodic_evaluation_id") + .references(() => periodicEvaluations.id, { onDelete: "cascade" }) + .notNull(), + + // 리뷰어 정보 (evaluationTargetReviewers 참조) + evaluationTargetReviewerId: integer("evaluation_target_reviewer_id") + .references(() => evaluationTargetReviewers.id, { onDelete: "cascade" }) + .notNull(), + + // 평가항목별 점수 (카테고리별 합산 점수) + processScore: decimal("process_score", { precision: 5, scale: 2 }), + priceScore: decimal("price_score", { precision: 5, scale: 2 }), + deliveryScore: decimal("delivery_score", { precision: 5, scale: 2 }), + selfEvaluationScore: decimal("self_evaluation_score", { precision: 5, scale: 2 }), + + // 가점/감점 + participationBonus: decimal("participation_bonus", { precision: 5, scale: 2 }).default("0"), + qualityDeduction: decimal("quality_deduction", { precision: 5, scale: 2 }).default("0"), + + // 리뷰어 총점 + totalScore: decimal("total_score", { precision: 5, scale: 2 }), + grade: varchar("grade", { + length: 5, + enum: ["S", "A", "B", "C", "D"] + }), + + // 평가 완료 여부 + isCompleted: boolean("is_completed").default(false), + completedAt: timestamp("completed_at"), + + // 리뷰어 의견 + reviewerComment: text("reviewer_comment"), + + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}, (table) => ({ + // 같은 평가에 같은 리뷰어는 하나의 평가만 + uniqueReviewerEvaluation: unique("unique_reviewer_evaluation") + .on(table.periodicEvaluationId, table.evaluationTargetReviewerId), +})); + +// 2-1. 리뷰어별 세부 평가 점수 테이블 (평가표의 각 항목별 점수) +export const reviewerEvaluationDetails = pgTable("reviewer_evaluation_details", { + id: serial("id").primaryKey(), + + // 리뷰어 평가 참조 + reviewerEvaluationId: integer("reviewer_evaluation_id") + .references(() => reviewerEvaluations.id, { onDelete: "cascade" }) + .notNull(), + + // 평가 기준 참조 + regEvalCriteriaDetailsId: integer("reg_eval_criteria_details_id") + .references(() => regEvalCriteriaDetails.id) + .notNull(), + + // 리뷰어가 매긴 점수 + score: decimal("score", { precision: 5, scale: 2 }).notNull(), + + // 세부 의견 + comment: text("comment"), + + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}, (table) => ({ + // 같은 리뷰어 평가에서 같은 평가 기준은 하나의 점수만 + uniqueReviewerCriteria: unique("unique_reviewer_criteria") + .on(table.reviewerEvaluationId, table.regEvalCriteriaDetailsId), +})); + +// 1. periodicEvaluations relations +export const periodicEvaluationsRelations = relations(periodicEvaluations, ({ one, many }) => ({ + // 평가 대상 + evaluationTarget: one(evaluationTargets, { + fields: [periodicEvaluations.evaluationTargetId], + references: [evaluationTargets.id], + }), + + // 최종 확정자 + finalizedByUser: one(users, { + fields: [periodicEvaluations.finalizedBy], + references: [users.id], + }), + + // 리뷰어별 평가들 + reviewerEvaluations: many(reviewerEvaluations), + +})); + +// 2. reviewerEvaluations relations +export const reviewerEvaluationsRelations = relations(reviewerEvaluations, ({ one, many }) => ({ + // 정기평가 + periodicEvaluation: one(periodicEvaluations, { + fields: [reviewerEvaluations.periodicEvaluationId], + references: [periodicEvaluations.id], + }), + + // 평가 대상 리뷰어 + evaluationTargetReviewer: one(evaluationTargetReviewers, { + fields: [reviewerEvaluations.evaluationTargetReviewerId], + references: [evaluationTargetReviewers.id], + }), + + // 세부 평가 점수들 + evaluationDetails: many(reviewerEvaluationDetails), +})); + +// 3. reviewerEvaluationDetails relations +export const reviewerEvaluationDetailsRelations = relations(reviewerEvaluationDetails, ({ one }) => ({ + // 리뷰어 평가 + reviewerEvaluation: one(reviewerEvaluations, { + fields: [reviewerEvaluationDetails.reviewerEvaluationId], + references: [reviewerEvaluations.id], + }), + + // 평가 기준 세부사항 + regEvalCriteriaDetail: one(regEvalCriteriaDetails, { + fields: [reviewerEvaluationDetails.regEvalCriteriaDetailsId], + references: [regEvalCriteriaDetails.id], + }), +})); + +// ---------------------------------------------------------------------------------------------------- + +/* TYPES */ +type PeriodicEvaluation = typeof periodicEvaluations.$inferSelect; +type NewPeriodicEvaluation = typeof periodicEvaluations.$inferInsert; + +type ReviewerEvaluation = typeof reviewerEvaluations.$inferSelect; +type NewReviewerEvaluation = typeof reviewerEvaluations.$inferInsert; + +type ReviewerEvaluationDetail = typeof reviewerEvaluationDetails.$inferSelect; +type NewReviewerEvaluationDetail = typeof reviewerEvaluationDetails.$inferInsert; + +// 관계 포함 타입들 +type PeriodicEvaluationWithRelations = PeriodicEvaluation & { + evaluationTarget?: typeof evaluationTargets.$inferSelect; + finalizedByUser?: typeof users.$inferSelect; + reviewerEvaluations?: ReviewerEvaluationWithRelations[]; +}; + +type ReviewerEvaluationWithRelations = ReviewerEvaluation & { + periodicEvaluation?: PeriodicEvaluation; + evaluationTargetReviewer?: typeof evaluationTargetReviewers.$inferSelect; + evaluationDetails?: ReviewerEvaluationDetailWithRelations[]; +}; + +type ReviewerEvaluationDetailWithRelations = ReviewerEvaluationDetail & { + reviewerEvaluation?: ReviewerEvaluation; + regEvalCriteriaDetail?: typeof regEvalCriteriaDetails.$inferSelect; +}; + +export const periodicEvaluationsView = pgView('periodic_evaluations_view').as((qb) => + qb + .select({ + // ═══════════════════════════════════════════════════════════════ + // 정기평가 기본 정보 (평가 대상 핵심 정보 포함) + // ═══════════════════════════════════════════════════════════════ + id: periodicEvaluations.id, + evaluationTargetId: periodicEvaluations.evaluationTargetId, + + // 평가 대상 핵심 정보 (조인으로 가져와서 기본 정보로 포함) + evaluationYear: evaluationTargets.evaluationYear, + division: evaluationTargets.division, + vendorId: evaluationTargets.vendorId, + vendorCode: evaluationTargets.vendorCode, + vendorName: evaluationTargets.vendorName, + domesticForeign: evaluationTargets.domesticForeign, + materialType: evaluationTargets.materialType, + + // 평가 기간 + evaluationPeriod: periodicEvaluations.evaluationPeriod, + + // 업체 제출 관련 + documentsSubmitted: periodicEvaluations.documentsSubmitted, + submissionDate: periodicEvaluations.submissionDate, + submissionDeadline: periodicEvaluations.submissionDeadline, + + // 평가 점수 (최종 확정) + finalScore: periodicEvaluations.finalScore, + finalGrade: periodicEvaluations.finalGrade, + + // 평가 점수 (평가자 평균) + evaluationScore: periodicEvaluations.evaluationScore, + evaluationGrade: periodicEvaluations.evaluationGrade, + + // 평가항목별 점수 + processScore: periodicEvaluations.processScore, + priceScore: periodicEvaluations.priceScore, + deliveryScore: periodicEvaluations.deliveryScore, + selfEvaluationScore: periodicEvaluations.selfEvaluationScore, + + // 합계 점수 + totalScore: periodicEvaluations.totalScore, + + // 가점/감점 + participationBonus: periodicEvaluations.participationBonus, + qualityDeduction: periodicEvaluations.qualityDeduction, + + // 평가 상태 + status: periodicEvaluations.status, + + // 평가 완료 정보 + reviewCompletedAt: periodicEvaluations.reviewCompletedAt, + finalizedAt: periodicEvaluations.finalizedAt, + finalizedBy: periodicEvaluations.finalizedBy, + + // 비고 + evaluationNote: periodicEvaluations.evaluationNote, + + // 생성/수정일 + createdAt: periodicEvaluations.createdAt, + updatedAt: periodicEvaluations.updatedAt, + + // ═══════════════════════════════════════════════════════════════ + // 평가 대상 추가 정보 (evaluationTargets 조인) + // ═══════════════════════════════════════════════════════════════ + evaluationTargetStatus: evaluationTargets.status, + evaluationTargetAdminComment: evaluationTargets.adminComment, + evaluationTargetConsolidatedComment: evaluationTargets.consolidatedComment, + evaluationTargetConsensusStatus: evaluationTargets.consensusStatus, + evaluationTargetConfirmedAt: evaluationTargets.confirmedAt, + + // ═══════════════════════════════════════════════════════════════ + // 리뷰어 통계 (서브쿼리로 계산) + // ═══════════════════════════════════════════════════════════════ + totalReviewers: sql<number>`( + SELECT COUNT(*)::int + FROM ${reviewerEvaluations} re + WHERE re.periodic_evaluation_id = ${periodicEvaluations.id} + )`.as('total_reviewers'), + + completedReviewers: sql<number>`( + SELECT COUNT(*)::int + FROM ${reviewerEvaluations} re + WHERE re.periodic_evaluation_id = ${periodicEvaluations.id} + AND re.is_completed = true + )`.as('completed_reviewers'), + + pendingReviewers: sql<number>`( + SELECT COUNT(*)::int + FROM ${reviewerEvaluations} re + WHERE re.periodic_evaluation_id = ${periodicEvaluations.id} + AND re.is_completed = false + )`.as('pending_reviewers'), + + // ═══════════════════════════════════════════════════════════════ + // 최종 확정자 정보 + // ═══════════════════════════════════════════════════════════════ + finalizedByUserName: users.name, + finalizedByUserEmail: users.email, + }) + .from(periodicEvaluations) + .leftJoin(evaluationTargets, eq(periodicEvaluations.evaluationTargetId, evaluationTargets.id)) + .leftJoin(users, eq(periodicEvaluations.finalizedBy, users.id)) + .orderBy(periodicEvaluations.createdAt) +); + + +// ================================================================ +// TYPES +// ================================================================ + +export type PeriodicEvaluationView = typeof periodicEvaluationsView.$inferSelect; diff --git a/db/schema/index.ts b/db/schema/index.ts index fc59692d..480207f9 100644 --- a/db/schema/index.ts +++ b/db/schema/index.ts @@ -20,6 +20,7 @@ export * from './bRfq'; export * from './techVendors'; export * from './evaluation'; export * from './evaluationTarget'; +export * from './projectGtc'; // MDG SOAP 수신용 // export * from './MDG/modelMaster' diff --git a/db/schema/projectGtc.ts b/db/schema/projectGtc.ts new file mode 100644 index 00000000..220e42df --- /dev/null +++ b/db/schema/projectGtc.ts @@ -0,0 +1,45 @@ +import { pgTable, pgView, timestamp, integer, varchar, serial } from 'drizzle-orm/pg-core'; +import { projects } from './projects'; +import { eq, sql } from "drizzle-orm"; + +export const projectGtcFiles = pgTable('project_gtc_files', { + id: serial("id").primaryKey(), + projectId: integer('project_id').references(() => projects.id).notNull(), + fileName: varchar("file_name", { length: 255 }).notNull(), + filePath: varchar("file_path", { length: 1024 }).notNull(), + originalFileName: varchar("original_file_name", { length: 255 }).notNull(), + fileSize: integer('file_size'), + mimeType: varchar("mime_type", { length: 100 }), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); + +// Project GTC 뷰 (프로젝트 정보와 파일 정보를 조인) +export const projectGtcView = pgView('project_gtc_view').as((qb) => { + return qb + .select({ + // 프로젝트 정보 + id: sql<number>`${projects.id}`.as('id'), + code: sql<string>`${projects.code}`.as('code'), + name: sql<string>`${projects.name}`.as('name'), + type: sql<string>`${projects.type}`.as('type'), + projectCreatedAt: sql<Date>`${projects.createdAt}`.as('project_created_at'), + projectUpdatedAt: sql<Date>`${projects.updatedAt}`.as('project_updated_at'), + + // GTC 파일 정보 + gtcFileId: sql<number | null>`${projectGtcFiles.id}`.as('gtc_file_id'), + fileName: sql<string | null>`${projectGtcFiles.fileName}`.as('fileName'), + filePath: sql<string | null>`${projectGtcFiles.filePath}`.as('filePath'), + originalFileName: sql<string | null>`${projectGtcFiles.originalFileName}`.as('originalFileName'), + fileSize: sql<number | null>`${projectGtcFiles.fileSize}`.as('fileSize'), + mimeType: sql<string | null>`${projectGtcFiles.mimeType}`.as('mimeType'), + gtcCreatedAt: sql<Date | null>`${projectGtcFiles.createdAt}`.as('gtcCreatedAt'), + gtcUpdatedAt: sql<Date | null>`${projectGtcFiles.updatedAt}`.as('gtcUpdatedAt'), + }) + .from(projects) + .leftJoin(projectGtcFiles, eq(projects.id, projectGtcFiles.projectId)); +}); + +// 타입 정의 +export type ProjectGtcFile = typeof projectGtcFiles.$inferSelect; +export type ProjectGtcView = typeof projectGtcView.$inferSelect;
\ No newline at end of file diff --git a/db/schema/techSales.ts b/db/schema/techSales.ts index 334bf6bb..744d22cc 100644 --- a/db/schema/techSales.ts +++ b/db/schema/techSales.ts @@ -34,6 +34,8 @@ import { integer, numeric, date, + json, + index, } from "drizzle-orm/pg-core"; import { relations } from "drizzle-orm"; import { biddingProjects } from "./projects"; @@ -60,6 +62,7 @@ export const TECH_SALES_QUOTATION_STATUSES = { SUBMITTED: "Submitted", REVISED: "Revised", ACCEPTED: "Accepted", + REJECTED: "Rejected", } as const; export type TechSalesQuotationStatus = typeof TECH_SALES_QUOTATION_STATUSES[keyof typeof TECH_SALES_QUOTATION_STATUSES]; @@ -90,6 +93,12 @@ export const TECH_SALES_QUOTATION_STATUS_CONFIG = { description: "승인된 견적서", color: "text-green-600", }, + [TECH_SALES_QUOTATION_STATUSES.REJECTED]: { + label: "거절됨", + variant: "destructive" as const, + description: "거절된 견적서", + color: "text-red-600", + }, } as const; // ===== 스키마 정의 ===== @@ -241,6 +250,37 @@ export const techSalesVendorQuotations = pgTable( } ); +// 기술영업 벤더 견적서 revision 히스토리 테이블 (이전 버전 스냅샷 저장) +export const techSalesVendorQuotationRevisions = pgTable( + "tech_sales_vendor_quotation_revisions", + { + id: serial("id").primaryKey(), + quotationId: integer("quotation_id") + .notNull() + .references(() => techSalesVendorQuotations.id, { onDelete: "cascade" }), + + // 버전 정보 + version: integer("version").notNull(), + + // 이전 데이터 JSON 스냅샷 + snapshot: json("snapshot").notNull(), + + // 변경 사유 + changeReason: text("change_reason"), + revisionNote: text("revision_note"), + + // 변경자 정보 + revisedBy: integer("revised_by"), + revisedAt: timestamp("revised_at").defaultNow().notNull(), + }, + (table) => ({ + quotationVersionIdx: index("tech_sales_quotation_revisions_quotation_version_idx").on( + table.quotationId, + table.version + ), + }) +); + export const techSalesRfqComments = pgTable( "tech_sales_rfq_comments", { @@ -299,6 +339,28 @@ export const techSalesRfqCommentAttachments = pgTable("tech_sales_rfq_comment_at uploadedAt: timestamp("uploaded_at").defaultNow().notNull(), }); +// 기술영업 벤더 견적서 첨부파일 테이블 +export const techSalesVendorQuotationAttachments = pgTable("tech_sales_vendor_quotation_attachments", { + id: serial("id").primaryKey(), + quotationId: integer("quotation_id") + .notNull() + .references(() => techSalesVendorQuotations.id, { onDelete: "cascade" }), + fileName: varchar("file_name", { length: 255 }).notNull(), + originalFileName: varchar("original_file_name", { length: 255 }).notNull(), + fileSize: integer("file_size").notNull(), + fileType: varchar("file_type", { length: 100 }), + filePath: varchar("file_path", { length: 500 }).notNull(), + description: text("description"), // 파일 설명 + uploadedBy: integer("uploaded_by").references(() => users.id, { + onDelete: "set null", + }), + vendorId: integer("vendor_id").references(() => techVendors.id, { + onDelete: "set null", + }), + isVendorUpload: boolean("is_vendor_upload").default(true), // 벤더가 업로드한 파일인지 + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); // 타입 정의 export type TechSalesVendorQuotations = @@ -390,6 +452,7 @@ export const techSalesVendorQuotationsRelations = relations(techSalesVendorQuota // 첨부파일 관계 attachments: many(techSalesRfqCommentAttachments), + quotationAttachments: many(techSalesVendorQuotationAttachments), })); export const techSalesAttachmentsRelations = relations(techSalesAttachments, ({ one }) => ({ @@ -474,4 +537,26 @@ export const techSalesRfqCommentAttachmentsRelations = relations(techSalesRfqCom fields: [techSalesRfqCommentAttachments.vendorId], references: [techVendors.id], }), +})); + +// 기술영업 벤더 견적서 첨부파일 relations +export const techSalesVendorQuotationAttachmentsRelations = relations(techSalesVendorQuotationAttachments, ({ one }) => ({ + // 견적서 관계 + quotation: one(techSalesVendorQuotations, { + fields: [techSalesVendorQuotationAttachments.quotationId], + references: [techSalesVendorQuotations.id], + }), + + // 업로드한 사용자 관계 + uploadedByUser: one(users, { + fields: [techSalesVendorQuotationAttachments.uploadedBy], + references: [users.id], + relationName: "techSalesQuotationAttachmentUploadedBy", + }), + + // 벤더 관계 + vendor: one(techVendors, { + fields: [techSalesVendorQuotationAttachments.vendorId], + references: [techVendors.id], + }), }));
\ No newline at end of file diff --git a/lib/evaluation-target-list/service.ts b/lib/evaluation-target-list/service.ts index 62f0f0ef..572b468d 100644 --- a/lib/evaluation-target-list/service.ts +++ b/lib/evaluation-target-list/service.ts @@ -1,13 +1,13 @@ 'use server' -import { and, or, desc, asc, ilike, eq, isNull, sql, count } from "drizzle-orm"; +import { and, or, desc, asc, ilike, eq, isNull, sql, count, inArray } from "drizzle-orm"; import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"; import { filterColumns } from "@/lib/filter-columns"; import db from "@/db/db"; -import { - evaluationTargets, - evaluationTargetReviewers, +import { + evaluationTargets, + evaluationTargetReviewers, evaluationTargetReviews, users, vendors, @@ -21,7 +21,9 @@ import { } from "@/db/schema"; import { GetEvaluationTargetsSchema } from "./validation"; import { PgTransaction } from "drizzle-orm/pg-core"; - +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { sendEmail } from "../mail/sendEmail"; export async function selectEvaluationTargetsFromView( tx: PgTransaction<any, any, any>, @@ -52,7 +54,7 @@ export async function countEvaluationTargetsFromView( .select({ count: count() }) .from(evaluationTargetsWithDepartments) .where(where); - + return res[0]?.count ?? 0; } @@ -102,9 +104,9 @@ export async function getEvaluationTargets(input: GetEvaluationTargetsSchema) { // 정렬 (View 테이블 기준) const orderBy = input.sort.length > 0 ? input.sort.map((item) => { - const column = evaluationTargetsWithDepartments[item.id as keyof typeof evaluationTargetsWithDepartments]; - return item.desc ? desc(column) : asc(column); - }) + const column = evaluationTargetsWithDepartments[item.id as keyof typeof evaluationTargetsWithDepartments]; + return item.desc ? desc(column) : asc(column); + }) : [desc(evaluationTargetsWithDepartments.createdAt)]; // 데이터 조회 - View 테이블 사용 @@ -196,7 +198,7 @@ export interface CreateEvaluationTargetInput { // service.ts 파일의 CreateEvaluationTargetInput 타입 수정 export interface CreateEvaluationTargetInput { evaluationYear: number - division: "OCEAN" | "SHIPYARD" + division: "PLANT" | "SHIP" vendorId: number materialType: "EQUIPMENT" | "BULK" | "EQUIPMENT_BULK" adminComment?: string @@ -216,17 +218,16 @@ export async function createEvaluationTarget( input: CreateEvaluationTargetInput, createdBy: number ) { - - console.log(input,"input") + console.log(input, "input") try { return await db.transaction(async (tx) => { - // 벤더 정보 조회 (기존과 동일) + // 벤더 정보 조회 const vendor = await tx .select({ id: vendors.id, vendorCode: vendors.vendorCode, vendorName: vendors.vendorName, - country: vendors.country, + country: vendors.country, }) .from(vendors) .where(eq(vendors.id, input.vendorId)) @@ -238,7 +239,7 @@ export async function createEvaluationTarget( const vendorInfo = vendor[0]; - // 중복 체크 (기존과 동일) + // 중복 체크 const existing = await tx .select({ id: evaluationTargets.id }) .from(evaluationTargets) @@ -255,51 +256,57 @@ export async function createEvaluationTarget( throw new Error("이미 동일한 평가 대상이 존재합니다."); } - // 평가 대상 생성 (기존과 동일) + // 🔧 수정: 타입 추론 문제 해결 + const targetValues: typeof evaluationTargets.$inferInsert = { + evaluationYear: input.evaluationYear, + division: input.division, + vendorId: input.vendorId, + vendorCode: vendorInfo.vendorCode ?? '', + vendorName: vendorInfo.vendorName, + domesticForeign: vendorInfo.country === 'KR' ? 'DOMESTIC' : 'FOREIGN', + materialType: input.materialType, + status: 'PENDING', + adminComment: input.adminComment, + adminUserId: createdBy, + ldClaimCount: input.ldClaimCount ?? 0, + // 🔧 수정: decimal 타입은 숫자로 처리 + ldClaimAmount: input.ldClaimAmount?.toString() ?? '0', + ldClaimCurrency: input.ldClaimCurrency ?? 'KRW', + } + + console.log(targetValues) + + // 평가 대상 생성 const newEvaluationTarget = await tx .insert(evaluationTargets) - .values({ - evaluationYear: input.evaluationYear, - division: input.division, - vendorId: input.vendorId, - vendorCode: vendorInfo.vendorCode || "", - vendorName: vendorInfo.vendorName, - domesticForeign: vendorInfo.country === "KR" ? "DOMESTIC" : "FOREIGN", - materialType: input.materialType, - status: "PENDING", - adminComment: input.adminComment, - adminUserId: createdBy, - ldClaimCount: input.ldClaimCount || 0, - ldClaimAmount: input.ldClaimAmount?.toString() || "0", - ldClaimCurrency: input.ldClaimCurrency || "KRW", - }) + .values(targetValues) .returning({ id: evaluationTargets.id }); const evaluationTargetId = newEvaluationTarget[0].id; - // ✅ 담당자들 지정 (departmentNameFrom 추가) + // 담당자들 지정 if (input.reviewers && input.reviewers.length > 0) { - // 담당자들의 부서 정보 조회 const reviewerIds = input.reviewers.map(r => r.reviewerUserId); + + // 🔧 수정: SQL 배열 처리 개선 const reviewerInfos = await tx .select({ id: users.id, - departmentName: users.departmentName, // users 테이블에 부서명 필드가 있다고 가정 }) .from(users) - .where(sql`${users.id} = ANY(${reviewerIds})`); - - const reviewerAssignments = input.reviewers.map((reviewer) => { - const reviewerInfo = reviewerInfos.find(info => info.id === reviewer.reviewerUserId); - - return { - evaluationTargetId, - departmentCode: reviewer.departmentCode, - departmentNameFrom: reviewerInfo?.departmentName || null, // ✅ 실제 부서명 저장 - reviewerUserId: reviewer.reviewerUserId, - assignedBy: createdBy, - }; - }); + .where(inArray(users.id, reviewerIds)); // sql 대신 inArray 사용 + + const reviewerAssignments: typeof evaluationTargetReviewers.$inferInsert[] = + input.reviewers.map(r => { + const info = reviewerInfos.find(i => i.id === r.reviewerUserId); + return { + evaluationTargetId, + departmentCode: r.departmentCode, + departmentNameFrom: info?.departmentName ?? "TEST 부서", + reviewerUserId: r.reviewerUserId, + assignedBy: createdBy, + }; + }); await tx.insert(evaluationTargetReviewers).values(reviewerAssignments); } @@ -319,6 +326,253 @@ export async function createEvaluationTarget( } } +//업데이트 입력 타입 정의 +export interface UpdateEvaluationTargetInput { + id: number + adminComment?: string + consolidatedComment?: string + ldClaimCount?: number + ldClaimAmount?: number + ldClaimCurrency?: "KRW" | "USD" | "EUR" | "JPY" + consensusStatus?: boolean | null + orderIsApproved?: boolean | null + procurementIsApproved?: boolean | null + qualityIsApproved?: boolean | null + designIsApproved?: boolean | null + csIsApproved?: boolean | null + // 담당자 이메일 변경 + orderReviewerEmail?: string + procurementReviewerEmail?: string + qualityReviewerEmail?: string + designReviewerEmail?: string + csReviewerEmail?: string +} + +export interface UpdateEvaluationTargetInput { + id: number + // 기본 정보 + adminComment?: string + consolidatedComment?: string + ldClaimCount?: number + ldClaimAmount?: number + ldClaimCurrency?: "KRW" | "USD" | "EUR" | "JPY" + consensusStatus?: boolean | null + + // 각 부서별 평가 결과 + orderIsApproved?: boolean | null + procurementIsApproved?: boolean | null + qualityIsApproved?: boolean | null + designIsApproved?: boolean | null + csIsApproved?: boolean | null + + // 담당자 이메일 (사용자 ID로 변환됨) + orderReviewerEmail?: string + procurementReviewerEmail?: string + qualityReviewerEmail?: string + designReviewerEmail?: string + csReviewerEmail?: string +} + +export async function updateEvaluationTarget(input: UpdateEvaluationTargetInput) { + console.log(input, "update input") + + try { + const session = await auth() + if (!session?.user) { + throw new Error("인증이 필요합니다.") + } + + return await db.transaction(async (tx) => { + // 평가 대상 존재 확인 + const existing = await tx + .select({ id: evaluationTargets.id }) + .from(evaluationTargets) + .where(eq(evaluationTargets.id, input.id)) + .limit(1) + + if (!existing.length) { + throw new Error("평가 대상을 찾을 수 없습니다.") + } + + // 1. 기본 정보 업데이트 + const updateFields: Partial<typeof evaluationTargets.$inferInsert> = {} + + if (input.adminComment !== undefined) { + updateFields.adminComment = input.adminComment + } + if (input.consolidatedComment !== undefined) { + updateFields.consolidatedComment = input.consolidatedComment + } + if (input.ldClaimCount !== undefined) { + updateFields.ldClaimCount = input.ldClaimCount + } + if (input.ldClaimAmount !== undefined) { + updateFields.ldClaimAmount = input.ldClaimAmount.toString() + } + if (input.ldClaimCurrency !== undefined) { + updateFields.ldClaimCurrency = input.ldClaimCurrency + } + if (input.consensusStatus !== undefined) { + updateFields.consensusStatus = input.consensusStatus + } + + // 기본 정보가 있으면 업데이트 + if (Object.keys(updateFields).length > 0) { + updateFields.updatedAt = new Date() + + await tx + .update(evaluationTargets) + .set(updateFields) + .where(eq(evaluationTargets.id, input.id)) + } + + // 2. 담당자 정보 업데이트 + const reviewerUpdates = [ + { departmentCode: EVALUATION_DEPARTMENT_CODES.ORDER_EVAL, email: input.orderReviewerEmail }, + { departmentCode: EVALUATION_DEPARTMENT_CODES.PROCUREMENT_EVAL, email: input.procurementReviewerEmail }, + { departmentCode: EVALUATION_DEPARTMENT_CODES.QUALITY_EVAL, email: input.qualityReviewerEmail }, + { departmentCode: EVALUATION_DEPARTMENT_CODES.DESIGN_EVAL, email: input.designReviewerEmail }, + { departmentCode: EVALUATION_DEPARTMENT_CODES.CS_EVAL, email: input.csReviewerEmail }, + ] + + for (const update of reviewerUpdates) { + if (update.email !== undefined) { + // 기존 담당자 제거 + await tx + .delete(evaluationTargetReviewers) + .where( + and( + eq(evaluationTargetReviewers.evaluationTargetId, input.id), + eq(evaluationTargetReviewers.departmentCode, update.departmentCode) + ) + ) + + // 새 담당자 추가 (이메일이 있는 경우만) + if (update.email) { + // 이메일로 사용자 ID 조회 + const user = await tx + .select({ id: users.id }) + .from(users) + .where(eq(users.email, update.email)) + .limit(1) + + if (user.length > 0) { + await tx + .insert(evaluationTargetReviewers) + .values({ + evaluationTargetId: input.id, + departmentCode: update.departmentCode, + reviewerUserId: user[0].id, + assignedBy: session.user.id, + }) + } + } + } + } + + // 3. 평가 결과 업데이트 + const reviewUpdates = [ + { departmentCode: EVALUATION_DEPARTMENT_CODES.ORDER_EVAL, isApproved: input.orderIsApproved }, + { departmentCode: EVALUATION_DEPARTMENT_CODES.PROCUREMENT_EVAL, isApproved: input.procurementIsApproved }, + { departmentCode: EVALUATION_DEPARTMENT_CODES.QUALITY_EVAL, isApproved: input.qualityIsApproved }, + { departmentCode: EVALUATION_DEPARTMENT_CODES.DESIGN_EVAL, isApproved: input.designIsApproved }, + { departmentCode: EVALUATION_DEPARTMENT_CODES.CS_EVAL, isApproved: input.csIsApproved }, + ] + + for (const review of reviewUpdates) { + if (review.isApproved !== undefined) { + // 해당 부서의 담당자 조회 + const reviewer = await tx + .select({ + reviewerUserId: evaluationTargetReviewers.reviewerUserId + }) + .from(evaluationTargetReviewers) + .where( + and( + eq(evaluationTargetReviewers.evaluationTargetId, input.id), + eq(evaluationTargetReviewers.departmentCode, review.departmentCode) + ) + ) + .limit(1) + + if (reviewer.length > 0) { + // 기존 평가 결과 삭제 + await tx + .delete(evaluationTargetReviews) + .where( + and( + eq(evaluationTargetReviews.evaluationTargetId, input.id), + eq(evaluationTargetReviews.reviewerUserId, reviewer[0].reviewerUserId) + ) + ) + + // 새 평가 결과 추가 (null이 아닌 경우만) + if (review.isApproved !== null) { + await tx + .insert(evaluationTargetReviews) + .values({ + evaluationTargetId: input.id, + reviewerUserId: reviewer[0].reviewerUserId, + departmentCode: review.departmentCode, + isApproved: review.isApproved, + reviewedAt: new Date(), + }) + } + } + } + } + + // 4. 의견 일치 상태 및 전체 상태 자동 계산 + const currentReviews = await tx + .select({ + isApproved: evaluationTargetReviews.isApproved, + departmentCode: evaluationTargetReviews.departmentCode, + }) + .from(evaluationTargetReviews) + .where(eq(evaluationTargetReviews.evaluationTargetId, input.id)) + + console.log("Current reviews:", currentReviews) + + // 최소 3개 부서에서 평가가 완료된 경우 의견 일치 상태 계산 + if (currentReviews.length >= 3) { + const approvals = currentReviews.map(r => r.isApproved) + const allApproved = approvals.every(approval => approval === true) + const allRejected = approvals.every(approval => approval === false) + const hasConsensus = allApproved || allRejected + + let newStatus: "PENDING" | "CONFIRMED" | "EXCLUDED" = "PENDING" + if (hasConsensus) { + newStatus = allApproved ? "CONFIRMED" : "EXCLUDED" + } + + console.log("Auto-updating status:", { hasConsensus, newStatus, approvals }) + + await tx + .update(evaluationTargets) + .set({ + consensusStatus: hasConsensus, + status: newStatus, + confirmedAt: hasConsensus ? new Date() : null, + confirmedBy: hasConsensus ? session.user.id : null, + updatedAt: new Date() + }) + .where(eq(evaluationTargets.id, input.id)) + } + + return { + success: true, + message: "평가 대상이 성공적으로 수정되었습니다.", + } + }) + } catch (error) { + console.error("Error updating evaluation target:", error) + return { + success: false, + error: error instanceof Error ? error.message : "평가 대상 수정 중 오류가 발생했습니다.", + } + } +} + // 담당자 목록 조회 시 부서 정보도 함께 반환 export async function getAvailableReviewers(departmentCode?: string) { try { @@ -358,9 +612,9 @@ export async function getAvailableVendors(search?: string) { // 검색어가 있으면 적용 search ? or( - ilike(vendors.vendorCode, `%${search}%`), - ilike(vendors.vendorName, `%${search}%`) - ) + ilike(vendors.vendorCode, `%${search}%`), + ilike(vendors.vendorName, `%${search}%`) + ) : undefined ) ) @@ -392,4 +646,266 @@ export async function getDepartmentInfo() { key, }; }); +} + + +export async function confirmEvaluationTargets(targetIds: number[]) { + try { + const session = await getServerSession(authOptions) + + if (!session?.user) { + return { success: false, error: "인증이 필요합니다." } + } + + if (targetIds.length === 0) { + return { success: false, error: "선택된 평가 대상이 없습니다." } + } + + // 트랜잭션으로 처리 + await db.transaction(async (tx) => { + // 확정 가능한 대상들 확인 (PENDING 상태이면서 consensusStatus가 true인 것들) + const eligibleTargets = await tx + .select() + .from(evaluationTargets) + .where( + and( + inArray(evaluationTargets.id, targetIds), + eq(evaluationTargets.status, "PENDING"), + eq(evaluationTargets.consensusStatus, true) + ) + ) + + if (eligibleTargets.length === 0) { + throw new Error("확정 가능한 평가 대상이 없습니다. (의견 일치 상태인 대기중 항목만 확정 가능)") + } + + // 상태를 CONFIRMED로 변경 + const confirmedTargetIds = eligibleTargets.map(target => target.id) + await tx + .update(evaluationTargets) + .set({ + status: "CONFIRMED", + confirmedAt: new Date(), + confirmedBy: Number(session.user.id), + updatedAt: new Date() + }) + .where(inArray(evaluationTargets.id, confirmedTargetIds)) + + return confirmedTargetIds + }) + + + return { + success: true, + message: `${targetIds.length}개 평가 대상이 확정되었습니다.`, + confirmedCount: targetIds.length + } + + } catch (error) { + console.error("Error confirming evaluation targets:", error) + return { + success: false, + error: error instanceof Error ? error.message : "확정 처리 중 오류가 발생했습니다." + } + } +} + +export async function excludeEvaluationTargets(targetIds: number[]) { + try { + const session = await getServerSession(authOptions) + if (!session?.user) { + return { success: false, error: "인증이 필요합니다." } + } + + if (targetIds.length === 0) { + return { success: false, error: "선택된 평가 대상이 없습니다." } + } + + // 트랜잭션으로 처리 + await db.transaction(async (tx) => { + // 제외 가능한 대상들 확인 (PENDING 상태인 것들) + const eligibleTargets = await tx + .select() + .from(evaluationTargets) + .where( + and( + inArray(evaluationTargets.id, targetIds), + eq(evaluationTargets.status, "PENDING") + ) + ) + + if (eligibleTargets.length === 0) { + throw new Error("제외 가능한 평가 대상이 없습니다. (대기중 상태인 항목만 제외 가능)") + } + + // 상태를 EXCLUDED로 변경 + const excludedTargetIds = eligibleTargets.map(target => target.id) + await tx + .update(evaluationTargets) + .set({ + status: "EXCLUDED", + updatedAt: new Date() + }) + .where(inArray(evaluationTargets.id, excludedTargetIds)) + + return excludedTargetIds + }) + + + return { + success: true, + message: `${targetIds.length}개 평가 대상이 제외되었습니다.`, + excludedCount: targetIds.length + } + + } catch (error) { + console.error("Error excluding evaluation targets:", error) + return { + success: false, + error: error instanceof Error ? error.message : "제외 처리 중 오류가 발생했습니다." + } + } +} + +export async function requestEvaluationReview(targetIds: number[], message?: string) { + try { + const session = await auth() + if (!session?.user) { + return { success: false, error: "인증이 필요합니다." } + } + + if (targetIds.length === 0) { + return { success: false, error: "선택된 평가 대상이 없습니다." } + } + + // 선택된 평가 대상들과 담당자 정보 조회 + const targetsWithReviewers = await db + .select({ + id: evaluationTargets.id, + vendorCode: evaluationTargets.vendorCode, + vendorName: evaluationTargets.vendorName, + materialType: evaluationTargets.materialType, + evaluationYear: evaluationTargets.evaluationYear, + status: evaluationTargets.status, + reviewerEmail: users.email, + reviewerName: users.name, + departmentCode: evaluationTargetReviewers.departmentCode, + departmentName: evaluationTargetReviewers.departmentNameFrom, + }) + .from(evaluationTargets) + .leftJoin( + evaluationTargetReviewers, + eq(evaluationTargets.id, evaluationTargetReviewers.evaluationTargetId) + ) + .leftJoin( + users, + eq(evaluationTargetReviewers.reviewerUserId, users.id) + ) + .where( + and( + inArray(evaluationTargets.id, targetIds), + eq(evaluationTargets.status, "PENDING") + ) + ) + + if (targetsWithReviewers.length === 0) { + return { success: false, error: "의견 요청 가능한 평가 대상이 없습니다." } + } + + // 평가 대상별로 그룹화 + const targetGroups = targetsWithReviewers.reduce((acc, item) => { + if (!acc[item.id]) { + acc[item.id] = { + id: item.id, + vendorCode: item.vendorCode, + vendorName: item.vendorName, + materialType: item.materialType, + evaluationYear: item.evaluationYear, + reviewers: [] + } + } + + if (item.reviewerEmail) { + acc[item.id].reviewers.push({ + email: item.reviewerEmail, + name: item.reviewerName, + departmentCode: item.departmentCode, + departmentName: item.departmentName + }) + } + + return acc + }, {} as Record<number, any>) + + const targets = Object.values(targetGroups) + + // 모든 담당자 이메일 수집 (중복 제거) + const reviewerEmails = new Set<string>() + const reviewerInfo = new Map<string, { name: string; departments: string[] }>() + + targets.forEach(target => { + target.reviewers.forEach((reviewer: any) => { + if (reviewer.email) { + reviewerEmails.add(reviewer.email) + + if (!reviewerInfo.has(reviewer.email)) { + reviewerInfo.set(reviewer.email, { + name: reviewer.name || reviewer.email, + departments: [] + }) + } + + const info = reviewerInfo.get(reviewer.email)! + if (reviewer.departmentName && !info.departments.includes(reviewer.departmentName)) { + info.departments.push(reviewer.departmentName) + } + } + }) + }) + + if (reviewerEmails.size === 0) { + return { success: false, error: "담당자가 지정되지 않은 평가 대상입니다." } + } + + // 각 담당자에게 이메일 발송 + const emailPromises = Array.from(reviewerEmails).map(email => { + const reviewer = reviewerInfo.get(email)! + + return sendEmail({ + to: email, + subject: `벤더 평가 의견 요청 - ${targets.length}건`, + template: "evaluation-review-request", + context: { + requesterName: session.user.name || session.user.email, + reviewerName: reviewer.name, + targetCount: targets.length, + targets: targets.map(target => ({ + vendorCode: target.vendorCode, + vendorName: target.vendorName, + materialType: target.materialType, + evaluationYear: target.evaluationYear + })), + message: message || "", + reviewUrl: `${process.env.NEXTAUTH_URL}/evaluation-targets`, + requestDate: new Date().toLocaleString('ko-KR') + } + }) + }) + + await Promise.all(emailPromises) + + revalidatePath("/evaluation-targets") + return { + success: true, + message: `${reviewerEmails.size}명의 담당자에게 의견 요청 이메일이 발송되었습니다.`, + emailCount: reviewerEmails.size + } + + } catch (error) { + console.error("Error requesting evaluation review:", error) + return { + success: false, + error: error instanceof Error ? error.message : "의견 요청 중 오류가 발생했습니다." + } + } }
\ No newline at end of file diff --git a/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx b/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx new file mode 100644 index 00000000..47af419d --- /dev/null +++ b/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx @@ -0,0 +1,384 @@ +// evaluation-target-action-dialogs.tsx +"use client" + +import * as React from "react" +import { Loader2, AlertTriangle, Check, X, MessageSquare } from "lucide-react" +import { toast } from "sonner" + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { Badge } from "@/components/ui/badge" + +import { + confirmEvaluationTargets, + excludeEvaluationTargets, + requestEvaluationReview +} from "../service" +import { EvaluationTargetWithDepartments } from "@/db/schema" + +// ---------------------------------------------------------------- +// 확정 컨펌 다이얼로그 +// ---------------------------------------------------------------- +interface ConfirmTargetsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + targets: EvaluationTargetWithDepartments[] + onSuccess?: () => void +} + +export function ConfirmTargetsDialog({ + open, + onOpenChange, + targets, + onSuccess +}: ConfirmTargetsDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + + // 확정 가능한 대상들 (consensusStatus가 true인 것들) + const confirmableTargets = targets.filter( + t => t.status === "PENDING" && t.consensusStatus === true + ) + + const handleConfirm = async () => { + if (confirmableTargets.length === 0) return + + setIsLoading(true) + try { + const targetIds = confirmableTargets.map(t => t.id) + const result = await confirmEvaluationTargets(targetIds) + + if (result.success) { + toast.success(result.message) + onSuccess?.() + onOpenChange(false) + } else { + toast.error(result.error) + } + } catch (error) { + toast.error("확정 처리 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + return ( + <AlertDialog open={open} onOpenChange={onOpenChange}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle className="flex items-center gap-2"> + <Check className="h-5 w-5 text-green-600" /> + 평가 대상 확정 + </AlertDialogTitle> + <AlertDialogDescription asChild> + <div className="space-y-3"> + <p> + 선택된 {targets.length}개 항목 중{" "} + <span className="font-semibold text-green-600"> + {confirmableTargets.length}개 항목 + </span> + 을 확정하시겠습니까? + </p> + + {confirmableTargets.length !== targets.length && ( + <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-200"> + <p className="text-sm text-yellow-800"> + <AlertTriangle className="h-4 w-4 inline mr-1" /> + 의견 일치 상태인 대기중 항목만 확정 가능합니다. + ({targets.length - confirmableTargets.length}개 항목 제외됨) + </p> + </div> + )} + + {confirmableTargets.length > 0 && ( + <div className="max-h-32 overflow-y-auto"> + <div className="text-sm space-y-1"> + {confirmableTargets.slice(0, 5).map(target => ( + <div key={target.id} className="flex items-center gap-2"> + <Badge variant="outline" className="text-xs"> + {target.vendorCode} + </Badge> + <span className="text-xs">{target.vendorName}</span> + </div> + ))} + {confirmableTargets.length > 5 && ( + <p className="text-xs text-muted-foreground"> + ...외 {confirmableTargets.length - 5}개 + </p> + )} + </div> + </div> + )} + </div> + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel> + <AlertDialogAction + onClick={handleConfirm} + disabled={isLoading || confirmableTargets.length === 0} + className="bg-green-600 hover:bg-green-700" + > + {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + 확정 ({confirmableTargets.length}) + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + ) +} + +// ---------------------------------------------------------------- +// 제외 컨펌 다이얼로그 +// ---------------------------------------------------------------- +interface ExcludeTargetsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + targets: EvaluationTargetWithDepartments[] + onSuccess?: () => void +} + +export function ExcludeTargetsDialog({ + open, + onOpenChange, + targets, + onSuccess +}: ExcludeTargetsDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + + // 제외 가능한 대상들 (PENDING 상태인 것들) + const excludableTargets = targets.filter(t => t.status === "PENDING") + + const handleExclude = async () => { + if (excludableTargets.length === 0) return + + setIsLoading(true) + try { + const targetIds = excludableTargets.map(t => t.id) + const result = await excludeEvaluationTargets(targetIds) + + if (result.success) { + toast.success(result.message) + onSuccess?.() + onOpenChange(false) + } else { + toast.error(result.error) + } + } catch (error) { + toast.error("제외 처리 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + return ( + <AlertDialog open={open} onOpenChange={onOpenChange}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle className="flex items-center gap-2"> + <X className="h-5 w-5 text-red-600" /> + 평가 대상 제외 + </AlertDialogTitle> + <AlertDialogDescription asChild> + <div className="space-y-3"> + <p> + 선택된 {targets.length}개 항목 중{" "} + <span className="font-semibold text-red-600"> + {excludableTargets.length}개 항목 + </span> + 을 제외하시겠습니까? + </p> + + {excludableTargets.length !== targets.length && ( + <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-200"> + <p className="text-sm text-yellow-800"> + <AlertTriangle className="h-4 w-4 inline mr-1" /> + 대기중 상태인 항목만 제외 가능합니다. + ({targets.length - excludableTargets.length}개 항목 제외됨) + </p> + </div> + )} + + {excludableTargets.length > 0 && ( + <div className="max-h-32 overflow-y-auto"> + <div className="text-sm space-y-1"> + {excludableTargets.slice(0, 5).map(target => ( + <div key={target.id} className="flex items-center gap-2"> + <Badge variant="outline" className="text-xs"> + {target.vendorCode} + </Badge> + <span className="text-xs">{target.vendorName}</span> + </div> + ))} + {excludableTargets.length > 5 && ( + <p className="text-xs text-muted-foreground"> + ...외 {excludableTargets.length - 5}개 + </p> + )} + </div> + </div> + )} + </div> + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel> + <AlertDialogAction + onClick={handleExclude} + disabled={isLoading || excludableTargets.length === 0} + className="bg-red-600 hover:bg-red-700" + > + {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + 제외 ({excludableTargets.length}) + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + ) +} + +// ---------------------------------------------------------------- +// 의견 요청 다이얼로그 +// ---------------------------------------------------------------- +interface RequestReviewDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + targets: EvaluationTargetWithDepartments[] + onSuccess?: () => void +} + +export function RequestReviewDialog({ + open, + onOpenChange, + targets, + onSuccess +}: RequestReviewDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [message, setMessage] = React.useState("") + + // 의견 요청 가능한 대상들 (PENDING 상태인 것들) + const reviewableTargets = targets.filter(t => t.status === "PENDING") + + // 담당자 이메일들 수집 + const reviewerEmails = React.useMemo(() => { + const emails = new Set<string>() + reviewableTargets.forEach(target => { + if (target.orderReviewerEmail) emails.add(target.orderReviewerEmail) + if (target.procurementReviewerEmail) emails.add(target.procurementReviewerEmail) + if (target.qualityReviewerEmail) emails.add(target.qualityReviewerEmail) + if (target.designReviewerEmail) emails.add(target.designReviewerEmail) + if (target.csReviewerEmail) emails.add(target.csReviewerEmail) + }) + return Array.from(emails) + }, [reviewableTargets]) + + const handleRequestReview = async () => { + if (reviewableTargets.length === 0) return + + setIsLoading(true) + try { + const targetIds = reviewableTargets.map(t => t.id) + const result = await requestEvaluationReview(targetIds, message) + + if (result.success) { + toast.success(result.message) + onSuccess?.() + onOpenChange(false) + setMessage("") + } else { + toast.error(result.error) + } + } catch (error) { + toast.error("의견 요청 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-md"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <MessageSquare className="h-5 w-5 text-blue-600" /> + 평가 의견 요청 + </DialogTitle> + <DialogDescription> + 선택된 평가 대상에 대한 의견을 담당자들에게 요청합니다. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {/* 요약 정보 */} + <div className="p-3 bg-blue-50 rounded-lg border border-blue-200"> + <div className="text-sm space-y-1"> + <p> + <span className="font-medium">요청 대상:</span> {reviewableTargets.length}개 평가 항목 + </p> + <p> + <span className="font-medium">받는 사람:</span> {reviewerEmails.length}명의 담당자 + </p> + </div> + </div> + + {/* 메시지 입력 */} + <div className="space-y-2"> + <Label htmlFor="review-message">추가 메시지 (선택사항)</Label> + <Textarea + id="review-message" + placeholder="담당자들에게 전달할 추가 메시지를 입력하세요..." + value={message} + onChange={(e) => setMessage(e.target.value)} + rows={3} + /> + </div> + + {reviewableTargets.length !== targets.length && ( + <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-200"> + <p className="text-sm text-yellow-800"> + <AlertTriangle className="h-4 w-4 inline mr-1" /> + 대기중 상태인 항목만 의견 요청 가능합니다. + ({targets.length - reviewableTargets.length}개 항목 제외됨) + </p> + </div> + )} + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button + onClick={handleRequestReview} + disabled={isLoading || reviewableTargets.length === 0} + > + {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + 의견 요청 발송 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file diff --git a/lib/evaluation-target-list/table/evaluation-target-table.tsx b/lib/evaluation-target-list/table/evaluation-target-table.tsx index 15837733..fe0b3188 100644 --- a/lib/evaluation-target-list/table/evaluation-target-table.tsx +++ b/lib/evaluation-target-list/table/evaluation-target-table.tsx @@ -25,6 +25,7 @@ import { getEvaluationTargetsColumns } from "./evaluation-targets-columns" import { EvaluationTargetsTableToolbarActions } from "./evaluation-targets-toolbar-actions" import { EvaluationTargetFilterSheet } from "./evaluation-targets-filter-sheet" import { EvaluationTargetWithDepartments } from "@/db/schema" +import { EditEvaluationTargetSheet } from "./update-evaluation-target" interface EvaluationTargetsTableProps { promises: Promise<[Awaited<ReturnType<typeof getEvaluationTargets>>]> @@ -40,13 +41,13 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number }) React.useEffect(() => { let isMounted = true - + async function fetchStats() { try { setIsLoading(true) setError(null) const statsData = await getEvaluationTargetsStats(evaluationYear) - + if (isMounted) { setStats(statsData) } @@ -186,45 +187,59 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number }) export function EvaluationTargetsTable({ promises, evaluationYear, className }: EvaluationTargetsTableProps) { const [rowAction, setRowAction] = React.useState<DataTableRowAction<EvaluationTargetWithDepartments> | null>(null) const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false) - console.count("E Targets render"); const router = useRouter() const searchParams = useSearchParams() const containerRef = React.useRef<HTMLDivElement>(null) const [containerTop, setContainerTop] = React.useState(0) + // ✅ 스크롤 이벤트 throttling으로 성능 최적화 const updateContainerBounds = React.useCallback(() => { if (containerRef.current) { const rect = containerRef.current.getBoundingClientRect() - setContainerTop(rect.top) + const newTop = rect.top + + // ✅ 값이 실제로 변경될 때만 상태 업데이트 + setContainerTop(prevTop => { + if (Math.abs(prevTop - newTop) > 1) { // 1px 이상 차이날 때만 업데이트 + return newTop + } + return prevTop + }) } }, []) + // ✅ throttle 함수 추가 + const throttledUpdateBounds = React.useCallback(() => { + let timeoutId: NodeJS.Timeout + return () => { + clearTimeout(timeoutId) + timeoutId = setTimeout(updateContainerBounds, 16) // ~60fps + } + }, [updateContainerBounds]) + React.useEffect(() => { updateContainerBounds() - + + const throttledHandler = throttledUpdateBounds() + const handleResize = () => { updateContainerBounds() } - + window.addEventListener('resize', handleResize) - window.addEventListener('scroll', updateContainerBounds) - + window.addEventListener('scroll', throttledHandler) // ✅ throttled 함수 사용 + return () => { window.removeEventListener('resize', handleResize) - window.removeEventListener('scroll', updateContainerBounds) + window.removeEventListener('scroll', throttledHandler) } - }, [updateContainerBounds]) + }, [updateContainerBounds, throttledUpdateBounds]) const [promiseData] = React.use(promises) const tableData = promiseData - console.log("Evaluation Targets Table Data:", { - dataLength: tableData.data?.length, - pageCount: tableData.pageCount, - total: tableData.total, - sampleData: tableData.data?.[0] - }) + console.log(tableData) const initialSettings = React.useMemo(() => ({ page: parseInt(searchParams.get('page') || '1'), @@ -232,7 +247,7 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: sort: searchParams.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "createdAt", desc: true }], filters: searchParams.get('filters') ? JSON.parse(searchParams.get('filters')!) : [], joinOperator: (searchParams.get('joinOperator') as "and" | "or") || "and", - basicFilters: searchParams.get('basicFilters') ? + basicFilters: searchParams.get('basicFilters') ? JSON.parse(searchParams.get('basicFilters')!) : [], basicJoinOperator: (searchParams.get('basicJoinOperator') as "and" | "or") || "and", search: searchParams.get('search') || '', @@ -259,8 +274,8 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: } = useTablePresets<EvaluationTargetWithDepartments>('evaluation-targets-table', initialSettings) const columns = React.useMemo( - () => getEvaluationTargetsColumns(), - [] + () => getEvaluationTargetsColumns({ setRowAction }), + [setRowAction] ) const filterFields: DataTableFilterField<EvaluationTargetWithDepartments>[] = [ @@ -271,31 +286,41 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: const advancedFilterFields: DataTableAdvancedFilterField<EvaluationTargetWithDepartments>[] = [ { id: "evaluationYear", label: "평가년도", type: "number" }, - { id: "division", label: "구분", type: "select", options: [ - { label: "해양", value: "OCEAN" }, - { label: "조선", value: "SHIPYARD" }, - ]}, + { + id: "division", label: "구분", type: "select", options: [ + { label: "해양", value: "OCEAN" }, + { label: "조선", value: "SHIPYARD" }, + ] + }, { id: "vendorCode", label: "벤더 코드", type: "text" }, { id: "vendorName", label: "벤더명", type: "text" }, - { id: "domesticForeign", label: "내외자", type: "select", options: [ - { label: "내자", value: "DOMESTIC" }, - { label: "외자", value: "FOREIGN" }, - ]}, - { id: "materialType", label: "자재구분", type: "select", options: [ - { label: "기자재", value: "EQUIPMENT" }, - { label: "벌크", value: "BULK" }, - { label: "기자재/벌크", value: "EQUIPMENT_BULK" }, - ]}, - { id: "status", label: "상태", type: "select", options: [ - { label: "검토 중", value: "PENDING" }, - { label: "확정", value: "CONFIRMED" }, - { label: "제외", value: "EXCLUDED" }, - ]}, - { id: "consensusStatus", label: "의견 일치", type: "select", options: [ - { label: "의견 일치", value: "true" }, - { label: "의견 불일치", value: "false" }, - { label: "검토 중", value: "null" }, - ]}, + { + id: "domesticForeign", label: "내외자", type: "select", options: [ + { label: "내자", value: "DOMESTIC" }, + { label: "외자", value: "FOREIGN" }, + ] + }, + { + id: "materialType", label: "자재구분", type: "select", options: [ + { label: "기자재", value: "EQUIPMENT" }, + { label: "벌크", value: "BULK" }, + { label: "기자재/벌크", value: "EQUIPMENT_BULK" }, + ] + }, + { + id: "status", label: "상태", type: "select", options: [ + { label: "검토 중", value: "PENDING" }, + { label: "확정", value: "CONFIRMED" }, + { label: "제외", value: "EXCLUDED" }, + ] + }, + { + id: "consensusStatus", label: "의견 일치", type: "select", options: [ + { label: "의견 일치", value: "true" }, + { label: "의견 불일치", value: "false" }, + { label: "검토 중", value: "null" }, + ] + }, { id: "adminComment", label: "관리자 의견", type: "text" }, { id: "consolidatedComment", label: "종합 의견", type: "text" }, { id: "confirmedAt", label: "확정일", type: "date" }, @@ -305,17 +330,21 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: const currentSettings = useMemo(() => { return getCurrentSettings() }, [getCurrentSettings]) - + + function getColKey<T>(c: ColumnDef<T>): string | undefined { + if ("accessorKey" in c && c.accessorKey) return c.accessorKey as string + if ("id" in c && c.id) return c.id as string + return undefined + } + const initialState = useMemo(() => { return { - sorting: initialSettings.sort.filter(sortItem => { - const columnExists = columns.some(col => col.accessorKey === sortItem.id || col.id === sortItem.id) - return columnExists - }) as any, + sorting: initialSettings.sort.filter(s => + columns.some(c => getColKey(c) === s.id)), columnVisibility: currentSettings.columnVisibility, columnPinning: currentSettings.pinnedColumns, } - }, [currentSettings, initialSettings.sort, columns]) + }, [columns, currentSettings, initialSettings.sort]) const { table } = useDataTable({ data: tableData.data, @@ -349,12 +378,12 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: return ( <> {/* Filter Panel */} - <div + <div className={cn( "fixed left-0 bg-background border-r z-50 flex flex-col transition-all duration-300 ease-in-out overflow-hidden", isFilterPanelOpen ? "border-r shadow-lg" : "border-r-0" )} - style={{ + style={{ width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px', top: `${containerTop}px`, height: `calc(100vh - ${containerTop}px)` @@ -362,7 +391,7 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: > <div className="h-full"> <EvaluationTargetFilterSheet - isOpen={isFilterPanelOpen} + isOpen={isFilterPanelOpen} onClose={() => setIsFilterPanelOpen(false)} onSearch={handleSearch} isLoading={false} @@ -371,12 +400,12 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: </div> {/* Main Content Container */} - <div + <div ref={containerRef} className={cn("relative w-full overflow-hidden", className)} > <div className="flex w-full h-full"> - <div + <div className="flex flex-col min-w-0 overflow-hidden transition-all duration-300 ease-in-out" style={{ width: isFilterPanelOpen ? `calc(100% - ${FILTER_PANEL_WIDTH}px)` : '100%', @@ -386,14 +415,14 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: {/* Header Bar */} <div className="flex items-center justify-between p-4 bg-background shrink-0"> <div className="flex items-center gap-3"> - <Button - variant="outline" - size="sm" + <Button + variant="outline" + size="sm" type='button' onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)} className="flex items-center shadow-sm" > - {isFilterPanelOpen ? <PanelLeftClose className="size-4"/> : <PanelLeftOpen className="size-4"/>} + {isFilterPanelOpen ? <PanelLeftClose className="size-4" /> : <PanelLeftOpen className="size-4" />} {getActiveBasicFilterCount() > 0 && ( <span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs"> {getActiveBasicFilterCount()} @@ -401,7 +430,7 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: )} </Button> </div> - + <div className="text-sm text-muted-foreground"> {tableData && ( <span>총 {tableData.total || tableData.data.length}건</span> @@ -437,11 +466,18 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: onSetDefaultPreset={setDefaultPreset} onRenamePreset={renamePreset} /> - + <EvaluationTargetsTableToolbarActions table={table} /> </div> </DataTableAdvancedToolbar> </DataTable> + + <EditEvaluationTargetSheet + open={rowAction?.type === "update"} + onOpenChange={() => setRowAction(null)} + evaluationTarget={rowAction?.row.original ?? null} + /> + </div> </div> </div> diff --git a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx index b1e19434..93807ef9 100644 --- a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx +++ b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx @@ -7,6 +7,11 @@ import { Button } from "@/components/ui/button"; import { Pencil, Eye, MessageSquare, Check, X } from "lucide-react"; import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"; import { EvaluationTargetWithDepartments } from "@/db/schema"; +import { EditEvaluationTargetSheet } from "./update-evaluation-target"; + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<EvaluationTargetWithDepartments> | null>>; +} // 상태별 색상 매핑 const getStatusBadgeVariant = (status: string) => { @@ -36,8 +41,8 @@ const getConsensusBadge = (consensusStatus: boolean | null) => { // 구분 배지 const getDivisionBadge = (division: string) => { return ( - <Badge variant={division === "OCEAN" ? "default" : "secondary"}> - {division === "OCEAN" ? "해양" : "조선"} + <Badge variant={division === "PLANT" ? "default" : "secondary"}> + {division === "PLANT" ? "해양" : "조선"} </Badge> ); }; @@ -46,7 +51,7 @@ const getDivisionBadge = (division: string) => { const getMaterialTypeBadge = (materialType: string) => { const typeMap = { EQUIPMENT: "기자재", - BULK: "벌크", + BULK: "벌크", EQUIPMENT_BULK: "기자재/벌크" }; return <Badge variant="outline">{typeMap[materialType] || materialType}</Badge>; @@ -61,8 +66,23 @@ const getDomesticForeignBadge = (domesticForeign: string) => { ); }; -export function getEvaluationTargetsColumns(): ColumnDef<EvaluationTargetWithDepartments>[] { +// 평가 상태 배지 +const getApprovalBadge = (isApproved: boolean | null) => { + if (isApproved === null) { + return <Badge variant="outline" className="text-xs">대기중</Badge>; + } + if (isApproved === true) { + return <Badge variant="default" className="bg-green-600 text-xs">승인</Badge>; + } + return <Badge variant="destructive" className="text-xs">거부</Badge>; +}; + +export function getEvaluationTargetsColumns({setRowAction}:GetColumnsProps): ColumnDef<EvaluationTargetWithDepartments>[] { return [ + // ═══════════════════════════════════════════════════════════════ + // 기본 정보 + // ═══════════════════════════════════════════════════════════════ + // Checkbox { id: "select", @@ -102,46 +122,6 @@ export function getEvaluationTargetsColumns(): ColumnDef<EvaluationTargetWithDep cell: ({ row }) => getDivisionBadge(row.getValue("division")), size: 80, }, - - // ░░░ 벤더 코드 ░░░ - { - accessorKey: "vendorCode", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더 코드" />, - cell: ({ row }) => ( - <span className="font-mono text-sm">{row.getValue("vendorCode")}</span> - ), - size: 120, - }, - - // ░░░ 벤더명 ░░░ - { - accessorKey: "vendorName", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />, - cell: ({ row }) => ( - <div className="truncate max-w-[200px]" title={row.getValue<string>("vendorName")!}> - {row.getValue("vendorName") as string} - </div> - ), - size: 200, - }, - - // ░░░ 내외자 ░░░ - { - accessorKey: "domesticForeign", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내외자" />, - cell: ({ row }) => getDomesticForeignBadge(row.getValue("domesticForeign")), - size: 80, - }, - - // ░░░ 자재구분 ░░░ - { - accessorKey: "materialType", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재구분" />, - cell: ({ row }) => getMaterialTypeBadge(row.getValue("materialType")), - size: 120, - }, - - // ░░░ 상태 ░░░ { accessorKey: "status", header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="상태" />, @@ -161,6 +141,54 @@ export function getEvaluationTargetsColumns(): ColumnDef<EvaluationTargetWithDep size: 100, }, + // ░░░ 벤더 코드 ░░░ + + { + header: "협력업체 정보", + columns: [ + { + accessorKey: "vendorCode", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더 코드" />, + cell: ({ row }) => ( + <span className="font-mono text-sm">{row.getValue("vendorCode")}</span> + ), + size: 120, + }, + + // ░░░ 벤더명 ░░░ + { + accessorKey: "vendorName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />, + cell: ({ row }) => ( + <div className="truncate max-w-[200px]" title={row.getValue<string>("vendorName")!}> + {row.getValue("vendorName") as string} + </div> + ), + size: 200, + }, + + // ░░░ 내외자 ░░░ + { + accessorKey: "domesticForeign", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내외자" />, + cell: ({ row }) => getDomesticForeignBadge(row.getValue("domesticForeign")), + size: 80, + }, + + ] + }, + + // ░░░ 자재구분 ░░░ + { + accessorKey: "materialType", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재구분" />, + cell: ({ row }) => getMaterialTypeBadge(row.getValue("materialType")), + size: 120, + }, + + // ░░░ 상태 ░░░ + + // ░░░ 의견 일치 여부 ░░░ { accessorKey: "consensusStatus", @@ -169,56 +197,235 @@ export function getEvaluationTargetsColumns(): ColumnDef<EvaluationTargetWithDep size: 100, }, - // ░░░ 담당자 현황 ░░░ + // ═══════════════════════════════════════════════════════════════ + // 주문 부서 그룹 + // ═══════════════════════════════════════════════════════════════ { - id: "reviewers", - header: "담당자 현황", - cell: ({ row }) => { - const reviewers = row.original.reviewers || []; - const totalReviewers = reviewers.length; - const completedReviews = reviewers.filter(r => r.review?.isApproved !== null).length; - const approvedReviews = reviewers.filter(r => r.review?.isApproved === true).length; - - return ( - <div className="flex items-center gap-2"> - <div className="text-xs"> - <span className="text-green-600 font-medium">{approvedReviews}</span> - <span className="text-muted-foreground">/{completedReviews}</span> - <span className="text-muted-foreground">/{totalReviewers}</span> - </div> - {totalReviewers > 0 && ( - <div className="flex gap-1"> - {reviewers.slice(0, 3).map((reviewer, idx) => ( - <div - key={idx} - className={`w-2 h-2 rounded-full ${ - reviewer.review?.isApproved === true - ? "bg-green-500" - : reviewer.review?.isApproved === false - ? "bg-red-500" - : "bg-gray-300" - }`} - title={`${reviewer.departmentCode}: ${ - reviewer.review?.isApproved === true - ? "승인" - : reviewer.review?.isApproved === false - ? "거부" - : "대기중" - }`} - /> - ))} - {totalReviewers > 3 && ( - <span className="text-xs text-muted-foreground">+{totalReviewers - 3}</span> - )} + header: "발주 평가 담당자", + columns: [ + { + accessorKey: "orderDepartmentName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />, + cell: ({ row }) => { + const departmentName = row.getValue<string>("orderDepartmentName"); + return departmentName ? ( + <div className="truncate max-w-[120px]" title={departmentName}> + {departmentName} </div> - )} - </div> - ); - }, - size: 120, - enableSorting: false, + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 120, + }, + { + accessorKey: "orderReviewerName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />, + cell: ({ row }) => { + const reviewerName = row.getValue<string>("orderReviewerName"); + return reviewerName ? ( + <div className="truncate max-w-[100px]" title={reviewerName}> + {reviewerName} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 100, + }, + { + accessorKey: "orderIsApproved", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />, + cell: ({ row }) => getApprovalBadge(row.getValue("orderIsApproved")), + size: 80, + }, + ], + }, + + // ═══════════════════════════════════════════════════════════════ + // 조달 부서 그룹 + // ═══════════════════════════════════════════════════════════════ + { + header: "조달 평가 담당자", + columns: [ + { + accessorKey: "procurementDepartmentName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />, + cell: ({ row }) => { + const departmentName = row.getValue<string>("procurementDepartmentName"); + return departmentName ? ( + <div className="truncate max-w-[120px]" title={departmentName}> + {departmentName} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 120, + }, + { + accessorKey: "procurementReviewerName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />, + cell: ({ row }) => { + const reviewerName = row.getValue<string>("procurementReviewerName"); + return reviewerName ? ( + <div className="truncate max-w-[100px]" title={reviewerName}> + {reviewerName} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 100, + }, + { + accessorKey: "procurementIsApproved", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />, + cell: ({ row }) => getApprovalBadge(row.getValue("procurementIsApproved")), + size: 80, + }, + ], }, + // ═══════════════════════════════════════════════════════════════ + // 품질 부서 그룹 + // ═══════════════════════════════════════════════════════════════ + { + header: "품질 평가 담당자", + columns: [ + { + accessorKey: "qualityDepartmentName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />, + cell: ({ row }) => { + const departmentName = row.getValue<string>("qualityDepartmentName"); + return departmentName ? ( + <div className="truncate max-w-[120px]" title={departmentName}> + {departmentName} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 120, + }, + { + accessorKey: "qualityReviewerName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />, + cell: ({ row }) => { + const reviewerName = row.getValue<string>("qualityReviewerName"); + return reviewerName ? ( + <div className="truncate max-w-[100px]" title={reviewerName}> + {reviewerName} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 100, + }, + { + accessorKey: "qualityIsApproved", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />, + cell: ({ row }) => getApprovalBadge(row.getValue("qualityIsApproved")), + size: 80, + }, + ], + }, + + // ═══════════════════════════════════════════════════════════════ + // 설계 부서 그룹 + // ═══════════════════════════════════════════════════════════════ + { + header: "설계 평가 담당자", + columns: [ + { + accessorKey: "designDepartmentName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />, + cell: ({ row }) => { + const departmentName = row.getValue<string>("designDepartmentName"); + return departmentName ? ( + <div className="truncate max-w-[120px]" title={departmentName}> + {departmentName} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 120, + }, + { + accessorKey: "designReviewerName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />, + cell: ({ row }) => { + const reviewerName = row.getValue<string>("designReviewerName"); + return reviewerName ? ( + <div className="truncate max-w-[100px]" title={reviewerName}> + {reviewerName} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 100, + }, + { + accessorKey: "designIsApproved", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />, + cell: ({ row }) => getApprovalBadge(row.getValue("designIsApproved")), + size: 80, + }, + ], + }, + + // ═══════════════════════════════════════════════════════════════ + // CS 부서 그룹 + // ═══════════════════════════════════════════════════════════════ + { + header: "CS 평가 담당자", + columns: [ + { + accessorKey: "csDepartmentName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="부서명" />, + cell: ({ row }) => { + const departmentName = row.getValue<string>("csDepartmentName"); + return departmentName ? ( + <div className="truncate max-w-[120px]" title={departmentName}> + {departmentName} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 120, + }, + { + accessorKey: "csReviewerName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자" />, + cell: ({ row }) => { + const reviewerName = row.getValue<string>("csReviewerName"); + return reviewerName ? ( + <div className="truncate max-w-[100px]" title={reviewerName}> + {reviewerName} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 100, + }, + { + accessorKey: "csIsApproved", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가" />, + cell: ({ row }) => getApprovalBadge(row.getValue("csIsApproved")), + size: 80, + }, + ], + }, + + // ═══════════════════════════════════════════════════════════════ + // 관리 정보 + // ═══════════════════════════════════════════════════════════════ + // ░░░ 관리자 의견 ░░░ { accessorKey: "adminComment", @@ -274,69 +481,47 @@ export function getEvaluationTargetsColumns(): ColumnDef<EvaluationTargetWithDep size: 100, }, + // ░░░ 생성일 ░░░ + { + accessorKey: "createdAt", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="생성일" />, + cell: ({ row }) => { + const createdAt = row.getValue<Date>("createdAt"); + return createdAt ? ( + <span className="text-sm"> + {new Intl.DateTimeFormat("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }).format(new Date(createdAt))} + </span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 100, + }, + // ░░░ Actions ░░░ { id: "actions", enableHiding: false, - size: 120, - minSize: 120, + size: 40, + minSize: 40, cell: ({ row }) => { - const record = row.original; - const [openDetail, setOpenDetail] = React.useState(false); - const [openEdit, setOpenEdit] = React.useState(false); - const [openRequest, setOpenRequest] = React.useState(false); - - return ( + return ( <div className="flex items-center gap-1"> <Button variant="ghost" size="icon" className="size-8" - onClick={() => setOpenDetail(true)} - aria-label="상세보기" - title="상세보기" - > - <Eye className="size-4" /> - </Button> - - <Button - variant="ghost" - size="icon" - className="size-8" - onClick={() => setOpenEdit(true)} + onClick={() => setRowAction({ row, type: "update" })} aria-label="수정" title="수정" > <Pencil className="size-4" /> </Button> - <Button - variant="ghost" - size="icon" - className="size-8" - onClick={() => setOpenRequest(true)} - aria-label="의견요청" - title="의견요청" - > - <MessageSquare className="size-4" /> - </Button> - - {/* TODO: 실제 다이얼로그 컴포넌트들로 교체 */} - {openDetail && ( - <div onClick={() => setOpenDetail(false)}> - {/* <EvaluationTargetDetailDialog /> */} - </div> - )} - {openEdit && ( - <div onClick={() => setOpenEdit(false)}> - {/* <EditEvaluationTargetDialog /> */} - </div> - )} - {openRequest && ( - <div onClick={() => setOpenRequest(false)}> - {/* <RequestReviewDialog /> */} - </div> - )} </div> ); }, diff --git a/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx b/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx index c14ae83f..502ee974 100644 --- a/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx +++ b/lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx @@ -44,12 +44,17 @@ const evaluationTargetFilterSchema = z.object({ vendorCode: z.string().optional(), vendorName: z.string().optional(), reviewerUserId: z.string().optional(), // 담당자 ID로 필터링 + orderReviewerName: z.string().optional(), // 주문 검토자명 + procurementReviewerName: z.string().optional(), // 조달 검토자명 + qualityReviewerName: z.string().optional(), // 품질 검토자명 + designReviewerName: z.string().optional(), // 설계 검토자명 + csReviewerName: z.string().optional(), // CS 검토자명 }) // 옵션 정의 const divisionOptions = [ - { value: "OCEAN", label: "해양" }, - { value: "SHIPYARD", label: "조선" }, + { value: "PLANT", label: "해양" }, + { value: "SHIP", label: "조선" }, ] const statusOptions = [ @@ -128,6 +133,11 @@ export function EvaluationTargetFilterSheet({ vendorCode: "", vendorName: "", reviewerUserId: "", + orderReviewerName: "", + procurementReviewerName: "", + qualityReviewerName: "", + designReviewerName: "", + csReviewerName: "", }, }) @@ -261,6 +271,57 @@ export function EvaluationTargetFilterSheet({ }) } + // 새로 추가된 검토자명 필터들 + if (data.orderReviewerName?.trim()) { + newFilters.push({ + id: "orderReviewerName", + value: data.orderReviewerName.trim(), + type: "text", + operator: "iLike", + rowId: generateId() + }) + } + + if (data.procurementReviewerName?.trim()) { + newFilters.push({ + id: "procurementReviewerName", + value: data.procurementReviewerName.trim(), + type: "text", + operator: "iLike", + rowId: generateId() + }) + } + + if (data.qualityReviewerName?.trim()) { + newFilters.push({ + id: "qualityReviewerName", + value: data.qualityReviewerName.trim(), + type: "text", + operator: "iLike", + rowId: generateId() + }) + } + + if (data.designReviewerName?.trim()) { + newFilters.push({ + id: "designReviewerName", + value: data.designReviewerName.trim(), + type: "text", + operator: "iLike", + rowId: generateId() + }) + } + + if (data.csReviewerName?.trim()) { + newFilters.push({ + id: "csReviewerName", + value: data.csReviewerName.trim(), + type: "text", + operator: "iLike", + rowId: generateId() + }) + } + // URL 업데이트 const currentUrl = new URL(window.location.href); const params = new URLSearchParams(currentUrl.search); @@ -313,6 +374,11 @@ export function EvaluationTargetFilterSheet({ vendorCode: "", vendorName: "", reviewerUserId: "", + orderReviewerName: "", + procurementReviewerName: "", + qualityReviewerName: "", + designReviewerName: "", + csReviewerName: "", }); // URL 초기화 @@ -723,6 +789,191 @@ export function EvaluationTargetFilterSheet({ )} /> + {/* 주문 검토자명 */} + <FormField + control={form.control} + name="orderReviewerName" + render={({ field }) => ( + <FormItem> + <FormLabel>발주 담당자명</FormLabel> + <FormControl> + <div className="relative"> + <Input + placeholder="발주 담당자명 입력" + {...field} + className={cn(field.value && "pr-8", "bg-white")} + disabled={isInitializing} + /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="absolute right-0 top-0 h-full px-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("orderReviewerName", ""); + }} + disabled={isInitializing} + > + <X className="size-3.5" /> + </Button> + )} + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 조달 검토자명 */} + <FormField + control={form.control} + name="procurementReviewerName" + render={({ field }) => ( + <FormItem> + <FormLabel>조달 담당자명</FormLabel> + <FormControl> + <div className="relative"> + <Input + placeholder="조달 담당자명 입력" + {...field} + className={cn(field.value && "pr-8", "bg-white")} + disabled={isInitializing} + /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="absolute right-0 top-0 h-full px-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("procurementReviewerName", ""); + }} + disabled={isInitializing} + > + <X className="size-3.5" /> + </Button> + )} + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 품질 검토자명 */} + <FormField + control={form.control} + name="qualityReviewerName" + render={({ field }) => ( + <FormItem> + <FormLabel>품질 담당자명</FormLabel> + <FormControl> + <div className="relative"> + <Input + placeholder="품질 담당자명 입력" + {...field} + className={cn(field.value && "pr-8", "bg-white")} + disabled={isInitializing} + /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="absolute right-0 top-0 h-full px-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("qualityReviewerName", ""); + }} + disabled={isInitializing} + > + <X className="size-3.5" /> + </Button> + )} + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 설계 검토자명 */} + <FormField + control={form.control} + name="designReviewerName" + render={({ field }) => ( + <FormItem> + <FormLabel>설계 담당자명</FormLabel> + <FormControl> + <div className="relative"> + <Input + placeholder="설계 담당자명 입력" + {...field} + className={cn(field.value && "pr-8", "bg-white")} + disabled={isInitializing} + /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="absolute right-0 top-0 h-full px-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("designReviewerName", ""); + }} + disabled={isInitializing} + > + <X className="size-3.5" /> + </Button> + )} + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* CS 검토자명 */} + <FormField + control={form.control} + name="csReviewerName" + render={({ field }) => ( + <FormItem> + <FormLabel>CS 담당자명</FormLabel> + <FormControl> + <div className="relative"> + <Input + placeholder="CS 담당자명 입력" + {...field} + className={cn(field.value && "pr-8", "bg-white")} + disabled={isInitializing} + /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="absolute right-0 top-0 h-full px-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("csReviewerName", ""); + }} + disabled={isInitializing} + > + <X className="size-3.5" /> + </Button> + )} + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> </div> diff --git a/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx b/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx index 3fb47771..9043c588 100644 --- a/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx +++ b/lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx @@ -24,7 +24,13 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { ManualCreateEvaluationTargetDialog } from "./manual-create-evaluation-target-dialog" +import { + ConfirmTargetsDialog, + ExcludeTargetsDialog, + RequestReviewDialog +} from "./evaluation-target-action-dialogs" import { EvaluationTargetWithDepartments } from "@/db/schema" +import { exportTableToExcel } from "@/lib/export" interface EvaluationTargetsTableToolbarActionsProps { table: Table<EvaluationTargetWithDepartments> @@ -37,6 +43,9 @@ export function EvaluationTargetsTableToolbarActions({ }: EvaluationTargetsTableToolbarActionsProps) { const [isLoading, setIsLoading] = React.useState(false) const [manualCreateDialogOpen, setManualCreateDialogOpen] = React.useState(false) + const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false) + const [excludeDialogOpen, setExcludeDialogOpen] = React.useState(false) + const [reviewDialogOpen, setReviewDialogOpen] = React.useState(false) const router = useRouter() // 선택된 행들 @@ -91,84 +100,12 @@ export function EvaluationTargetsTableToolbarActions({ } // ---------------------------------------------------------------- - // 선택된 항목들 확정 + // 다이얼로그 성공 핸들러 // ---------------------------------------------------------------- - const handleConfirmSelected = async () => { - if (!hasSelection || !selectedStats.canConfirm) return - - setIsLoading(true) - try { - // TODO: 확정 API 호출 - const confirmableTargets = selectedTargets.filter( - t => t.status === "PENDING" && t.consensusStatus === true - ) - - toast.success(`${confirmableTargets.length}개 항목이 확정되었습니다.`) - table.resetRowSelection() - router.refresh() - } catch (error) { - console.error('Error confirming targets:', error) - toast.error("확정 처리 중 오류가 발생했습니다.") - } finally { - setIsLoading(false) - } - } - - // ---------------------------------------------------------------- - // 선택된 항목들 제외 - // ---------------------------------------------------------------- - const handleExcludeSelected = async () => { - if (!hasSelection || !selectedStats.canExclude) return - - setIsLoading(true) - try { - // TODO: 제외 API 호출 - const excludableTargets = selectedTargets.filter(t => t.status === "PENDING") - - toast.success(`${excludableTargets.length}개 항목이 제외되었습니다.`) - table.resetRowSelection() - router.refresh() - } catch (error) { - console.error('Error excluding targets:', error) - toast.error("제외 처리 중 오류가 발생했습니다.") - } finally { - setIsLoading(false) - } - } - - // ---------------------------------------------------------------- - // 선택된 항목들 의견 요청 - // ---------------------------------------------------------------- - const handleRequestReview = async () => { - if (!hasSelection || !selectedStats.canRequestReview) return - - // TODO: 의견 요청 다이얼로그 열기 - toast.info("의견 요청 다이얼로그를 구현해주세요.") - } - - // ---------------------------------------------------------------- - // Excel 내보내기 - // ---------------------------------------------------------------- - const handleExport = () => { - try { - // TODO: Excel 내보내기 구현 - toast.success("Excel 파일이 다운로드되었습니다.") - } catch (error) { - console.error('Error exporting to Excel:', error) - toast.error("Excel 내보내기 중 오류가 발생했습니다.") - } - } - - // ---------------------------------------------------------------- - // 새로고침 - // ---------------------------------------------------------------- - const handleRefresh = () => { - if (onRefresh) { - onRefresh() - } else { - router.refresh() - } - toast.success("데이터가 새로고침되었습니다.") + const handleActionSuccess = () => { + table.resetRowSelection() + onRefresh?.() + router.refresh() } return ( @@ -204,22 +141,17 @@ export function EvaluationTargetsTableToolbarActions({ <Button variant="outline" size="sm" - onClick={handleExport} + onClick={() => + exportTableToExcel(table, { + filename: "vendor-target-list", + excludeColumns: ["select", "actions"], + }) + } className="gap-2" > <Download className="size-4" aria-hidden="true" /> <span className="hidden sm:inline">내보내기</span> </Button> - - <Button - variant="outline" - size="sm" - onClick={handleRefresh} - className="gap-2" - > - <RefreshCw className="size-4" aria-hidden="true" /> - <span className="hidden sm:inline">새로고침</span> - </Button> </div> {/* 선택된 항목 액션 버튼들 */} @@ -231,7 +163,7 @@ export function EvaluationTargetsTableToolbarActions({ variant="default" size="sm" className="gap-2 bg-green-600 hover:bg-green-700" - onClick={handleConfirmSelected} + onClick={() => setConfirmDialogOpen(true)} disabled={isLoading} > <Check className="size-4" aria-hidden="true" /> @@ -247,7 +179,7 @@ export function EvaluationTargetsTableToolbarActions({ variant="destructive" size="sm" className="gap-2" - onClick={handleExcludeSelected} + onClick={() => setExcludeDialogOpen(true)} disabled={isLoading} > <X className="size-4" aria-hidden="true" /> @@ -263,7 +195,7 @@ export function EvaluationTargetsTableToolbarActions({ variant="outline" size="sm" className="gap-2" - onClick={handleRequestReview} + onClick={() => setReviewDialogOpen(true)} disabled={isLoading} > <MessageSquare className="size-4" aria-hidden="true" /> @@ -282,6 +214,30 @@ export function EvaluationTargetsTableToolbarActions({ onOpenChange={setManualCreateDialogOpen} /> + {/* 확정 컨펌 다이얼로그 */} + <ConfirmTargetsDialog + open={confirmDialogOpen} + onOpenChange={setConfirmDialogOpen} + targets={selectedTargets} + onSuccess={handleActionSuccess} + /> + + {/* 제외 컨펌 다이얼로그 */} + <ExcludeTargetsDialog + open={excludeDialogOpen} + onOpenChange={setExcludeDialogOpen} + targets={selectedTargets} + onSuccess={handleActionSuccess} + /> + + {/* 의견 요청 다이얼로그 */} + <RequestReviewDialog + open={reviewDialogOpen} + onOpenChange={setReviewDialogOpen} + targets={selectedTargets} + onSuccess={handleActionSuccess} + /> + {/* 선택 정보 표시 */} {hasSelection && ( <div className="text-xs text-muted-foreground"> diff --git a/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx b/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx index 5704cba1..af369ea6 100644 --- a/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx +++ b/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx @@ -60,26 +60,7 @@ import { import { EVALUATION_TARGET_FILTER_OPTIONS, getDefaultEvaluationYear } from "../validation" import { useSession } from "next-auth/react" -// 폼 스키마 정의 -const createEvaluationTargetSchema = z.object({ - evaluationYear: z.number().min(2020).max(2030), - division: z.enum(["OCEAN", "SHIPYARD"]), - vendorId: z.number().min(1, "벤더를 선택해주세요"), - materialType: z.enum(["EQUIPMENT", "BULK", "EQUIPMENT_BULK"]), - adminComment: z.string().optional(), - // L/D 클레임 정보 - ldClaimCount: z.number().min(0).optional(), - ldClaimAmount: z.number().min(0).optional(), - ldClaimCurrency: z.enum(["KRW", "USD", "EUR", "JPY"]).optional(), - reviewers: z.array( - z.object({ - departmentCode: z.string(), - reviewerUserId: z.number().min(1, "담당자를 선택해주세요"), - }) - ).min(1, "최소 1명의 담당자를 지정해주세요"), -}) -type CreateEvaluationTargetFormValues = z.infer<typeof createEvaluationTargetSchema> interface ManualCreateEvaluationTargetDialogProps { open: boolean @@ -114,12 +95,49 @@ export function ManualCreateEvaluationTargetDialog({ // 부서 정보 상태 const [departments, setDepartments] = React.useState<Array<{ code: string, name: string, key: string }>>([]) + // 폼 스키마 정의 +const createEvaluationTargetSchema = z.object({ + evaluationYear: z.number().min(2020).max(2030), + division: z.enum(["PLANT", "SHIP"]), + vendorId: z.number().min(0), // 0도 허용, 클라이언트에서 검증 + materialType: z.enum(["EQUIPMENT", "BULK", "EQUIPMENT_BULK"]), + adminComment: z.string().optional(), + // L/D 클레임 정보 + ldClaimCount: z.number().min(0).optional(), + ldClaimAmount: z.number().min(0).optional(), + ldClaimCurrency: z.enum(["KRW", "USD", "EUR", "JPY"]).optional(), + reviewers: z.array( + z.object({ + departmentCode: z.string(), + reviewerUserId: z.number(), // min(1) 제거, 나중에 클라이언트에서 필터링 + }) + ), +}).refine((data) => { + // 벤더가 선택되어야 함 + if (data.vendorId === 0) { + return false; + } + // 최소 1명의 담당자가 지정되어야 함 (reviewerUserId > 0) + const validReviewers = data.reviewers + .filter(r => r.reviewerUserId > 0) + .map((r, i) => ({ + departmentCode: r.departmentCode || departments[i]?.code, // 없으면 보충 + reviewerUserId: r.reviewerUserId, + })); + return validReviewers.length > 0; +}, { + message: "벤더를 선택하고 최소 1명의 담당자를 지정해주세요.", + path: ["vendorId"] +}) +type CreateEvaluationTargetFormValues = z.infer<typeof createEvaluationTargetSchema> + + const form = useForm<CreateEvaluationTargetFormValues>({ resolver: zodResolver(createEvaluationTargetSchema), defaultValues: { evaluationYear: getDefaultEvaluationYear(), - division: "OCEAN", - vendorId: 0, + division: "SHIP", + vendorId: 0, // 임시로 0, 나중에 검증에서 체크 materialType: "EQUIPMENT", adminComment: "", ldClaimCount: 0, @@ -180,17 +198,12 @@ export function ManualCreateEvaluationTargetDialog({ // 부서 정보가 로드되면 reviewers 기본값 설정 React.useEffect(() => { if (departments.length > 0 && open) { - const currentReviewers = form.getValues("reviewers") - - // 이미 설정되어 있으면 다시 설정하지 않음 - if (currentReviewers.length === 0) { - const defaultReviewers = departments.map(dept => ({ - departmentCode: dept.code, - reviewerUserId: 0, - })) - form.setValue('reviewers', defaultReviewers) - } - } + const defaultReviewers = departments.map(dept => ({ + departmentCode: dept.code, // ✅ 반드시 포함 + reviewerUserId: 0, + })); + form.setValue("reviewers", defaultReviewers, { shouldValidate: false }); + } }, [departments, open]) // form 의존성 제거하고 조건 추가 console.log(departments) @@ -234,7 +247,7 @@ export function ManualCreateEvaluationTargetDialog({ // 폼과 상태 초기화 form.reset({ evaluationYear: getDefaultEvaluationYear(), - division: "OCEAN", + division: "SHIP", vendorId: 0, materialType: "EQUIPMENT", adminComment: "", @@ -269,7 +282,7 @@ export function ManualCreateEvaluationTargetDialog({ if (!open) { form.reset({ evaluationYear: getDefaultEvaluationYear(), - division: "OCEAN", + division: "SHIP", vendorId: 0, materialType: "EQUIPMENT", adminComment: "", @@ -326,24 +339,29 @@ export function ManualCreateEvaluationTargetDialog({ return ( <Dialog open={open} onOpenChange={handleOpenChange}> - <DialogContent className="max-w-lg flex flex-col h-[90vh]"> + <DialogContent className="max-w-lg h-[90vh] p-0 flex flex-col"> {/* 고정 헤더 */} - <DialogHeader className="flex-shrink-0"> - <DialogTitle>평가 대상 수동 생성</DialogTitle> - <DialogDescription> - 새로운 평가 대상을 수동으로 생성하고 담당자를 지정합니다. - </DialogDescription> - </DialogHeader> + <div className="flex-shrink-0 p-6 border-b"> + <DialogHeader> + <DialogTitle>평가 대상 수동 생성</DialogTitle> + <DialogDescription> + 새로운 평가 대상을 수동으로 생성하고 담당자를 지정합니다. + </DialogDescription> + </DialogHeader> + </div> {/* Form을 전체 콘텐츠를 감싸도록 수정 */} <Form {...form}> <form - onSubmit={form.handleSubmit(onSubmit)} - className="flex flex-col flex-1" + onSubmit={form.handleSubmit( + onSubmit, + (errors) => console.log('❌ validation errors:', errors) + )} + className="flex flex-col flex-1 min-h-0" id="evaluation-target-form" > {/* 스크롤 가능한 콘텐츠 영역 */} - <div className="flex-1 overflow-y-auto"> + <div className="flex-1 overflow-y-auto p-6"> <div className="space-y-6"> {/* 기본 정보 */} <Card> @@ -693,9 +711,14 @@ export function ManualCreateEvaluationTargetDialog({ <CommandItem value="선택 안함" onSelect={() => { - field.onChange(0) - setReviewerOpens(prev => ({...prev, [department.code]: false})) - }} + // reviewers[index] 전체를 갱신 + form.setValue( + `reviewers.${index}`, + { departmentCode: department.code, reviewerUserId: reviewer.id }, + { shouldValidate: true } + ); + setReviewerOpens(prev => ({ ...prev, [department.code]: false })); + }} > <Check className={cn( @@ -747,22 +770,24 @@ export function ManualCreateEvaluationTargetDialog({ </div> {/* 고정 버튼 영역 */} - <div className="flex-shrink-0 flex justify-end gap-3 pt-4 border-t"> - <Button - type="button" - variant="outline" - onClick={() => handleOpenChange(false)} - disabled={isSubmitting} - > - 취소 - </Button> - <Button - type="submit" - disabled={isSubmitting} - > - {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} - 생성 - </Button> + <div className="flex-shrink-0 border-t bg-background p-6"> + <div className="flex justify-end gap-3"> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={isSubmitting} + > + 취소 + </Button> + <Button + type="submit" + disabled={isSubmitting} + > + {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + 생성 + </Button> + </div> </div> </form> </Form> diff --git a/lib/evaluation-target-list/table/update-evaluation-target.tsx b/lib/evaluation-target-list/table/update-evaluation-target.tsx new file mode 100644 index 00000000..0d56addb --- /dev/null +++ b/lib/evaluation-target-list/table/update-evaluation-target.tsx @@ -0,0 +1,760 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { Check, ChevronsUpDown, Loader2, X } from "lucide-react" +import { toast } from "sonner" +import { useSession } from "next-auth/react" + +import { Button } from "@/components/ui/button" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { cn } from "@/lib/utils" + +import { + updateEvaluationTarget, + getAvailableReviewers, + getDepartmentInfo, + type UpdateEvaluationTargetInput, +} from "../service" +import { EvaluationTargetWithDepartments } from "@/db/schema" + +// 편집 가능한 필드들에 대한 스키마 +const editEvaluationTargetSchema = z.object({ + adminComment: z.string().optional(), + consolidatedComment: z.string().optional(), + ldClaimCount: z.number().min(0).optional(), + ldClaimAmount: z.number().min(0).optional(), + ldClaimCurrency: z.enum(["KRW", "USD", "EUR", "JPY"]).optional(), + consensusStatus: z.boolean().nullable().optional(), + orderIsApproved: z.boolean().nullable().optional(), + procurementIsApproved: z.boolean().nullable().optional(), + qualityIsApproved: z.boolean().nullable().optional(), + designIsApproved: z.boolean().nullable().optional(), + csIsApproved: z.boolean().nullable().optional(), + // 담당자 정보 수정 + orderReviewerEmail: z.string().optional(), + procurementReviewerEmail: z.string().optional(), + qualityReviewerEmail: z.string().optional(), + designReviewerEmail: z.string().optional(), + csReviewerEmail: z.string().optional(), +}) + +type EditEvaluationTargetFormValues = z.infer<typeof editEvaluationTargetSchema> + +interface EditEvaluationTargetSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + evaluationTarget: EvaluationTargetWithDepartments | null +} + +// 권한 타입 정의 +type PermissionLevel = "none" | "department" | "admin" + +interface UserPermissions { + level: PermissionLevel + editableApprovals: string[] // 편집 가능한 approval 필드들 +} + +export function EditEvaluationTargetSheet({ + open, + onOpenChange, + evaluationTarget, +}: EditEvaluationTargetSheetProps) { + const router = useRouter() + const [isSubmitting, setIsSubmitting] = React.useState(false) + const { data: session } = useSession() + + // 담당자 관련 상태 + const [reviewers, setReviewers] = React.useState<Array<{ id: number, name: string, email: string }>>([]) + const [isLoadingReviewers, setIsLoadingReviewers] = React.useState(false) + + // 각 부서별 담당자 선택 상태 + const [reviewerSearches, setReviewerSearches] = React.useState<Record<string, string>>({}) + const [reviewerOpens, setReviewerOpens] = React.useState<Record<string, boolean>>({}) + + // 부서 정보 상태 + const [departments, setDepartments] = React.useState<Array<{ code: string, name: string, key: string }>>([]) + + // 사용자 권한 계산 + const userPermissions = React.useMemo((): UserPermissions => { + if (!session?.user || !evaluationTarget) { + return { level: "none", editableApprovals: [] } + } + + const userEmail = session.user.email + const userRole = session.user.role + + // 평가관리자는 모든 권한 + if (userRole === "평가관리자") { + return { + level: "admin", + editableApprovals: [ + "orderIsApproved", + "procurementIsApproved", + "qualityIsApproved", + "designIsApproved", + "csIsApproved" + ] + } + } + + // 부서별 담당자 권한 확인 + const editableApprovals: string[] = [] + + if (evaluationTarget.orderReviewerEmail === userEmail) { + editableApprovals.push("orderIsApproved") + } + if (evaluationTarget.procurementReviewerEmail === userEmail) { + editableApprovals.push("procurementIsApproved") + } + if (evaluationTarget.qualityReviewerEmail === userEmail) { + editableApprovals.push("qualityIsApproved") + } + if (evaluationTarget.designReviewerEmail === userEmail) { + editableApprovals.push("designIsApproved") + } + if (evaluationTarget.csReviewerEmail === userEmail) { + editableApprovals.push("csIsApproved") + } + + return { + level: editableApprovals.length > 0 ? "department" : "none", + editableApprovals + } + }, [session, evaluationTarget]) + + const form = useForm<EditEvaluationTargetFormValues>({ + resolver: zodResolver(editEvaluationTargetSchema), + defaultValues: { + adminComment: "", + consolidatedComment: "", + ldClaimCount: 0, + ldClaimAmount: 0, + ldClaimCurrency: "KRW", + consensusStatus: null, + orderIsApproved: null, + procurementIsApproved: null, + qualityIsApproved: null, + designIsApproved: null, + csIsApproved: null, + orderReviewerEmail: "", + procurementReviewerEmail: "", + qualityReviewerEmail: "", + designReviewerEmail: "", + csReviewerEmail: "", + }, + }) + + // 부서 정보 로드 + const loadDepartments = React.useCallback(async () => { + try { + const departmentList = await getDepartmentInfo() + setDepartments(departmentList) + } catch (error) { + console.error("Error loading departments:", error) + toast.error("부서 정보를 불러오는데 실패했습니다.") + } + }, []) + + // 담당자 목록 로드 + const loadReviewers = React.useCallback(async () => { + setIsLoadingReviewers(true) + try { + const reviewerList = await getAvailableReviewers() + setReviewers(reviewerList) + } catch (error) { + console.error("Error loading reviewers:", error) + toast.error("담당자 목록을 불러오는데 실패했습니다.") + } finally { + setIsLoadingReviewers(false) + } + }, []) + + // 시트가 열릴 때 데이터 로드 및 폼 초기화 + React.useEffect(() => { + if (open && evaluationTarget) { + loadDepartments() + loadReviewers() + + // 폼에 기존 데이터 설정 + form.reset({ + adminComment: evaluationTarget.adminComment || "", + consolidatedComment: evaluationTarget.consolidatedComment || "", + ldClaimCount: evaluationTarget.ldClaimCount || 0, + ldClaimAmount: parseFloat(evaluationTarget.ldClaimAmount || "0"), + ldClaimCurrency: evaluationTarget.ldClaimCurrency || "KRW", + consensusStatus: evaluationTarget.consensusStatus, + orderIsApproved: evaluationTarget.orderIsApproved, + procurementIsApproved: evaluationTarget.procurementIsApproved, + qualityIsApproved: evaluationTarget.qualityIsApproved, + designIsApproved: evaluationTarget.designIsApproved, + csIsApproved: evaluationTarget.csIsApproved, + orderReviewerEmail: evaluationTarget.orderReviewerEmail || "", + procurementReviewerEmail: evaluationTarget.procurementReviewerEmail || "", + qualityReviewerEmail: evaluationTarget.qualityReviewerEmail || "", + designReviewerEmail: evaluationTarget.designReviewerEmail || "", + csReviewerEmail: evaluationTarget.csReviewerEmail || "", + }) + } + }, [open, evaluationTarget, form]) + + // 폼 제출 + async function onSubmit(data: EditEvaluationTargetFormValues) { + if (!evaluationTarget) return + + setIsSubmitting(true) + try { + const input: UpdateEvaluationTargetInput = { + id: evaluationTarget.id, + ...data, + } + + console.log("Updating evaluation target:", input) + + const result = await updateEvaluationTarget(input) + + if (result.success) { + toast.success(result.message || "평가 대상이 성공적으로 수정되었습니다.") + onOpenChange(false) + router.refresh() + } else { + toast.error(result.error || "평가 대상 수정에 실패했습니다.") + } + } catch (error) { + console.error("Error updating evaluation target:", error) + toast.error("평가 대상 수정 중 오류가 발생했습니다.") + } finally { + setIsSubmitting(false) + } + } + + // 시트 닫기 핸들러 + const handleOpenChange = (open: boolean) => { + onOpenChange(open) + if (!open) { + form.reset() + setReviewerSearches({}) + setReviewerOpens({}) + } + } + + // 담당자 검색 필터링 + const getFilteredReviewers = (search: string) => { + if (!search) return reviewers + return reviewers.filter(reviewer => + reviewer.name.toLowerCase().includes(search.toLowerCase()) || + reviewer.email.toLowerCase().includes(search.toLowerCase()) + ) + } + + // 필드 편집 권한 확인 + const canEditField = (fieldName: string): boolean => { + if (userPermissions.level === "admin") return true + if (userPermissions.level === "none") return false + + // 부서 담당자는 자신의 approval만 편집 가능 + return userPermissions.editableApprovals.includes(fieldName) + } + + // 관리자 전용 필드 확인 + const canEditAdminFields = (): boolean => { + return userPermissions.level === "admin" + } + + if (!evaluationTarget) { + return null + } + + // 권한이 없는 경우 + if (userPermissions.level === "none") { + return ( + <Sheet open={open} onOpenChange={handleOpenChange}> + <SheetContent className="sm:max-w-lg overflow-y-auto"> + <SheetHeader> + <SheetTitle>평가 대상 수정</SheetTitle> + <SheetDescription> + 권한이 없어 수정할 수 없습니다. + </SheetDescription> + </SheetHeader> + <div className="mt-6 p-4 bg-muted rounded-lg text-center"> + <p className="text-sm text-muted-foreground"> + 이 평가 대상을 수정할 권한이 없습니다. + </p> + </div> + </SheetContent> + </Sheet> + ) + } + + return ( + <Sheet open={open} onOpenChange={handleOpenChange}> + <SheetContent className="flex flex-col h-full sm:max-w-xl"> + <SheetHeader className="flex-shrink-0 text-left pb-6"> + <SheetTitle>평가 대상 수정</SheetTitle> + <SheetDescription> + 평가 대상 정보를 수정합니다. + {userPermissions.level === "department" && ( + <div className="mt-2 p-2 bg-blue-50 rounded text-sm"> + <strong>부서 담당자 권한:</strong> 해당 부서의 평가 항목만 수정 가능합니다. + </div> + )} + {userPermissions.level === "admin" && ( + <div className="mt-2 p-2 bg-green-50 rounded text-sm"> + <strong>평가관리자 권한:</strong> 모든 항목을 수정할 수 있습니다. + </div> + )} + </SheetDescription> + </SheetHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0"> + {/* 스크롤 가능한 컨텐츠 영역 */} + <div className="flex-1 overflow-y-auto pr-2 -mr-2"> + <div className="space-y-6"> + {/* 기본 정보 (읽기 전용) */} + <Card> + <CardHeader> + <CardTitle className="text-lg">기본 정보</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-2 gap-4 text-sm"> + <div> + <span className="font-medium">평가년도:</span> {evaluationTarget.evaluationYear} + </div> + <div> + <span className="font-medium">구분:</span> {evaluationTarget.division === "PLANT" ? "해양" : "조선"} + </div> + <div> + <span className="font-medium">벤더 코드:</span> {evaluationTarget.vendorCode} + </div> + <div> + <span className="font-medium">벤더명:</span> {evaluationTarget.vendorName} + </div> + <div> + <span className="font-medium">자재구분:</span> {evaluationTarget.materialType} + </div> + <div> + <span className="font-medium">상태:</span> {evaluationTarget.status} + </div> + </div> + </CardContent> + </Card> + + {/* L/D 클레임 정보 (관리자만) */} + {canEditAdminFields() && ( + <Card> + <CardHeader> + <CardTitle className="text-lg">L/D 클레임 정보</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-3 gap-4"> + <FormField + control={form.control} + name="ldClaimCount" + render={({ field }) => ( + <FormItem> + <FormLabel>클레임 건수</FormLabel> + <FormControl> + <Input + type="number" + min="0" + {...field} + onChange={(e) => field.onChange(parseInt(e.target.value) || 0)} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="ldClaimAmount" + render={({ field }) => ( + <FormItem> + <FormLabel>클레임 금액</FormLabel> + <FormControl> + <Input + type="number" + min="0" + step="0.01" + {...field} + onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="ldClaimCurrency" + render={({ field }) => ( + <FormItem> + <FormLabel>통화단위</FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectItem value="KRW">KRW (원)</SelectItem> + <SelectItem value="USD">USD (달러)</SelectItem> + <SelectItem value="EUR">EUR (유로)</SelectItem> + <SelectItem value="JPY">JPY (엔)</SelectItem> + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + </div> + </CardContent> + </Card> + )} + + {/* 평가 상태 */} + <Card> + <CardHeader> + <CardTitle className="text-lg">평가 상태</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + + {/* 의견 일치 여부 (관리자만) */} + {canEditAdminFields() && ( + <FormField + control={form.control} + name="consensusStatus" + render={({ field }) => ( + <FormItem> + <FormLabel>의견 일치 여부</FormLabel> + <Select + onValueChange={(value) => { + if (value === "null") field.onChange(null) + else field.onChange(value === "true") + }} + value={field.value === null ? "null" : field.value.toString()} + > + <FormControl> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectItem value="null">검토 중</SelectItem> + <SelectItem value="true">의견 일치</SelectItem> + <SelectItem value="false">의견 불일치</SelectItem> + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + )} + + {/* 각 부서별 평가 */} + <div className="grid grid-cols-1 gap-4"> + {[ + { key: "orderIsApproved", label: "주문 부서 평가", email: evaluationTarget.orderReviewerEmail }, + { key: "procurementIsApproved", label: "조달 부서 평가", email: evaluationTarget.procurementReviewerEmail }, + { key: "qualityIsApproved", label: "품질 부서 평가", email: evaluationTarget.qualityReviewerEmail }, + { key: "designIsApproved", label: "설계 부서 평가", email: evaluationTarget.designReviewerEmail }, + { key: "csIsApproved", label: "CS 부서 평가", email: evaluationTarget.csReviewerEmail }, + ].map(({ key, label, email }) => ( + <FormField + key={key} + control={form.control} + name={key as keyof EditEvaluationTargetFormValues} + render={({ field }) => ( + <FormItem> + <FormLabel className="flex items-center justify-between"> + <span>{label}</span> + {email && ( + <span className="text-xs text-muted-foreground"> + 담당자: {email} + </span> + )} + </FormLabel> + <Select + onValueChange={(value) => { + if (value === "null") field.onChange(null) + else field.onChange(value === "true") + }} + value={field.value === null ? "null" : field.value?.toString()} + disabled={!canEditField(key)} + > + <FormControl> + <SelectTrigger className={!canEditField(key) ? "opacity-50" : ""}> + <SelectValue /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectItem value="null">대기중</SelectItem> + <SelectItem value="true">평가대상 맞음</SelectItem> + <SelectItem value="false">평가대상 아님</SelectItem> + </SelectContent> + </Select> + {!canEditField(key) && ( + <p className="text-xs text-muted-foreground"> + 편집 권한이 없습니다. + </p> + )} + <FormMessage /> + </FormItem> + )} + /> + ))} + </div> + </CardContent> + </Card> + + {/* 의견 */} + <Card> + <CardHeader> + <CardTitle className="text-lg">의견</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + {/* 관리자 의견 (관리자만) */} + {canEditAdminFields() && ( + <FormField + control={form.control} + name="adminComment" + render={({ field }) => ( + <FormItem> + <FormLabel>관리자 의견</FormLabel> + <FormControl> + <Textarea + placeholder="관리자 의견을 입력하세요..." + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + + {/* 종합 의견 (모든 권한자) */} + <FormField + control={form.control} + name="consolidatedComment" + render={({ field }) => ( + <FormItem> + <FormLabel>종합 의견</FormLabel> + <FormControl> + <Textarea + placeholder="종합 의견을 입력하세요..." + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </CardContent> + </Card> + + {/* 담당자 변경 (관리자만) */} + {canEditAdminFields() && departments.length > 0 && ( + <Card> + <CardHeader> + <CardTitle className="text-lg">담당자 변경</CardTitle> + <p className="text-sm text-muted-foreground"> + 각 부서별 담당자를 변경할 수 있습니다. + </p> + </CardHeader> + <CardContent className="space-y-4"> + {[ + { key: "orderReviewerEmail", label: "주문 부서 담당자", current: evaluationTarget.orderReviewerEmail }, + { key: "procurementReviewerEmail", label: "조달 부서 담당자", current: evaluationTarget.procurementReviewerEmail }, + { key: "qualityReviewerEmail", label: "품질 부서 담당자", current: evaluationTarget.qualityReviewerEmail }, + { key: "designReviewerEmail", label: "설계 부서 담당자", current: evaluationTarget.designReviewerEmail }, + { key: "csReviewerEmail", label: "CS 부서 담당자", current: evaluationTarget.csReviewerEmail }, + ].map(({ key, label, current }) => { + const selectedReviewer = reviewers.find(r => r.email === form.watch(key as keyof EditEvaluationTargetFormValues)) + const filteredReviewers = getFilteredReviewers(reviewerSearches[key] || "") + + return ( + <FormField + key={key} + control={form.control} + name={key as keyof EditEvaluationTargetFormValues} + render={({ field }) => ( + <FormItem> + <FormLabel> + {label} + {current && ( + <span className="text-xs text-muted-foreground ml-2"> + (현재: {current}) + </span> + )} + </FormLabel> + <Popover + open={reviewerOpens[key] || false} + onOpenChange={(open) => setReviewerOpens(prev => ({...prev, [key]: open}))} + > + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + role="combobox" + aria-expanded={reviewerOpens[key]} + className="w-full justify-between" + > + {selectedReviewer ? ( + <span className="flex items-center gap-2"> + <span>{selectedReviewer.name}</span> + <span className="text-xs text-muted-foreground"> + ({selectedReviewer.email}) + </span> + </span> + ) : field.value ? ( + field.value + ) : ( + "담당자 선택..." + )} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-full p-0" align="start"> + <Command> + <CommandInput + placeholder="담당자 검색..." + value={reviewerSearches[key] || ""} + onValueChange={(value) => setReviewerSearches(prev => ({...prev, [key]: value}))} + /> + <CommandList> + <CommandEmpty> + {isLoadingReviewers ? ( + <div className="flex items-center gap-2"> + <Loader2 className="h-4 w-4 animate-spin" /> + 로딩 중... + </div> + ) : ( + "검색 결과가 없습니다." + )} + </CommandEmpty> + <CommandGroup> + <CommandItem + value="선택 안함" + onSelect={() => { + field.onChange("") + setReviewerOpens(prev => ({ ...prev, [key]: false })) + }} + > + <Check + className={cn( + "mr-2 h-4 w-4", + !field.value ? "opacity-100" : "opacity-0" + )} + /> + 선택 안함 + </CommandItem> + {filteredReviewers.map((reviewer) => ( + <CommandItem + key={reviewer.id} + value={`${reviewer.name} ${reviewer.email}`} + onSelect={() => { + field.onChange(reviewer.email) + setReviewerOpens(prev => ({...prev, [key]: false})) + }} + > + <Check + className={cn( + "mr-2 h-4 w-4", + reviewer.email === field.value ? "opacity-100" : "opacity-0" + )} + /> + <div className="flex items-center gap-2"> + <span>{reviewer.name}</span> + <span className="text-xs text-muted-foreground"> + ({reviewer.email}) + </span> + </div> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + ) + })} + </CardContent> + </Card> + )} + </div> + </div> + + {/* 고정된 버튼 영역 */} + <div className="flex-shrink-0 flex justify-end gap-3 pt-6 mt-6 border-t bg-background"> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={isSubmitting} + > + 취소 + </Button> + <Button + type="submit" + disabled={isSubmitting} + > + {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + 수정 + </Button> + </div> + </form> + </Form> + </SheetContent> + </Sheet> + ) +}
\ No newline at end of file diff --git a/lib/evaluation-target-list/validation.ts b/lib/evaluation-target-list/validation.ts index e42f536b..ce5604be 100644 --- a/lib/evaluation-target-list/validation.ts +++ b/lib/evaluation-target-list/validation.ts @@ -46,7 +46,7 @@ import { >; export type EvaluationTargetStatus = "PENDING" | "CONFIRMED" | "EXCLUDED"; - export type Division = "OCEAN" | "SHIPYARD"; + export type Division = "PLANT" | "SHIP"; export type MaterialType = "EQUIPMENT" | "BULK" | "EQUIPMENT_BULK"; export type DomesticForeign = "DOMESTIC" | "FOREIGN"; @@ -54,8 +54,8 @@ import { export const EVALUATION_TARGET_FILTER_OPTIONS = { DIVISIONS: [ - { value: "OCEAN", label: "해양" }, - { value: "SHIPYARD", label: "조선" }, + { value: "PLANT", label: "해양" }, + { value: "SHIP", label: "조선" }, ], STATUSES: [ { value: "PENDING", label: "검토 중" }, @@ -86,7 +86,7 @@ import { } export function validateDivision(division: string): division is Division { - return ["OCEAN", "SHIPYARD"].includes(division); + return ["PLANT", "SHIP"].includes(division); } export function validateStatus(status: string): status is EvaluationTargetStatus { @@ -142,8 +142,8 @@ import { // 구분별 라벨 반환 export function getDivisionLabel(division: Division): string { const divisionMap = { - OCEAN: "해양", - SHIPYARD: "조선" + PLANT: "해양", + SHIP: "조선" }; return divisionMap[division] || division; } diff --git a/lib/evaluation/service.ts b/lib/evaluation/service.ts new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/lib/evaluation/service.ts diff --git a/lib/evaluation/table/evaluation-columns.tsx b/lib/evaluation/table/evaluation-columns.tsx new file mode 100644 index 00000000..0c207a53 --- /dev/null +++ b/lib/evaluation/table/evaluation-columns.tsx @@ -0,0 +1,441 @@ +// ================================================================ +// 1. PERIODIC EVALUATIONS COLUMNS +// ================================================================ + +"use client"; +import * as React from "react"; +import { type ColumnDef } from "@tanstack/react-table"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Pencil, Eye, MessageSquare, Check, X, Clock, FileText } from "lucide-react"; +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"; +import { PeriodicEvaluationView } from "@/db/schema"; + + + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<PeriodicEvaluationView> | null>>; +} + +// 상태별 색상 매핑 +const getStatusBadgeVariant = (status: string) => { + switch (status) { + case "PENDING_SUBMISSION": + return "outline"; + case "SUBMITTED": + return "secondary"; + case "IN_REVIEW": + return "default"; + case "REVIEW_COMPLETED": + return "default"; + case "FINALIZED": + return "default"; + default: + return "outline"; + } +}; + +const getStatusLabel = (status: string) => { + const statusMap = { + PENDING_SUBMISSION: "제출대기", + SUBMITTED: "제출완료", + IN_REVIEW: "검토중", + REVIEW_COMPLETED: "검토완료", + FINALIZED: "최종확정" + }; + return statusMap[status] || status; +}; + +// 등급별 색상 +const getGradeBadgeVariant = (grade: string | null) => { + if (!grade) return "outline"; + switch (grade) { + case "S": + return "default"; + case "A": + return "secondary"; + case "B": + return "outline"; + case "C": + return "destructive"; + case "D": + return "destructive"; + default: + return "outline"; + } +}; + +// 구분 배지 +const getDivisionBadge = (division: string) => { + return ( + <Badge variant={division === "PLANT" ? "default" : "secondary"}> + {division === "PLANT" ? "해양" : "조선"} + </Badge> + ); +}; + +// 자재구분 배지 +const getMaterialTypeBadge = (materialType: string) => { + const typeMap = { + EQUIPMENT: "기자재", + BULK: "벌크", + EQUIPMENT_BULK: "기자재/벌크" + }; + return <Badge variant="outline">{typeMap[materialType] || materialType}</Badge>; +}; + +// 내외자 배지 +const getDomesticForeignBadge = (domesticForeign: string) => { + return ( + <Badge variant={domesticForeign === "DOMESTIC" ? "default" : "secondary"}> + {domesticForeign === "DOMESTIC" ? "내자" : "외자"} + </Badge> + ); +}; + +// 진행률 배지 +const getProgressBadge = (completed: number, total: number) => { + if (total === 0) return <Badge variant="outline">-</Badge>; + + const percentage = Math.round((completed / total) * 100); + const variant = percentage === 100 ? "default" : percentage >= 50 ? "secondary" : "destructive"; + + return <Badge variant={variant}>{completed}/{total} ({percentage}%)</Badge>; +}; + +export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): ColumnDef<PeriodicEvaluationWithRelations>[] { + return [ + // ═══════════════════════════════════════════════════════════════ + // 선택 및 기본 정보 + // ═══════════════════════════════════════════════════════════════ + + // Checkbox + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")} + onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)} + aria-label="select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(v) => row.toggleSelected(!!v)} + aria-label="select row" + className="translate-y-0.5" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + }, + + // ░░░ 평가년도 ░░░ + { + accessorKey: "evaluationTarget.evaluationYear", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가년도" />, + cell: ({ row }) => <span className="font-medium">{row.original.evaluationTarget?.evaluationYear}</span>, + size: 100, + }, + + // ░░░ 평가기간 ░░░ + { + accessorKey: "evaluationPeriod", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가기간" />, + cell: ({ row }) => ( + <Badge variant="outline">{row.getValue("evaluationPeriod")}</Badge> + ), + size: 100, + }, + + // ░░░ 구분 ░░░ + { + accessorKey: "evaluationTarget.division", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="구분" />, + cell: ({ row }) => getDivisionBadge(row.original.evaluationTarget?.division || ""), + size: 80, + }, + + // ═══════════════════════════════════════════════════════════════ + // 협력업체 정보 + // ═══════════════════════════════════════════════════════════════ + { + header: "협력업체 정보", + columns: [ + { + accessorKey: "evaluationTarget.vendorCode", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더 코드" />, + cell: ({ row }) => ( + <span className="font-mono text-sm">{row.original.evaluationTarget?.vendorCode}</span> + ), + size: 120, + }, + + { + accessorKey: "evaluationTarget.vendorName", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />, + cell: ({ row }) => ( + <div className="truncate max-w-[200px]" title={row.original.evaluationTarget?.vendorName}> + {row.original.evaluationTarget?.vendorName} + </div> + ), + size: 200, + }, + + { + accessorKey: "evaluationTarget.domesticForeign", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내외자" />, + cell: ({ row }) => getDomesticForeignBadge(row.original.evaluationTarget?.domesticForeign || ""), + size: 80, + }, + + { + accessorKey: "evaluationTarget.materialType", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재구분" />, + cell: ({ row }) => getMaterialTypeBadge(row.original.evaluationTarget?.materialType || ""), + size: 120, + }, + ] + }, + + // ═══════════════════════════════════════════════════════════════ + // 제출 현황 + // ═══════════════════════════════════════════════════════════════ + { + header: "제출 현황", + columns: [ + { + accessorKey: "documentsSubmitted", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="문서제출" />, + cell: ({ row }) => { + const submitted = row.getValue<boolean>("documentsSubmitted"); + return ( + <Badge variant={submitted ? "default" : "destructive"}> + {submitted ? "제출완료" : "미제출"} + </Badge> + ); + }, + size: 100, + }, + + { + accessorKey: "submissionDate", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="제출일" />, + cell: ({ row }) => { + const submissionDate = row.getValue<Date>("submissionDate"); + return submissionDate ? ( + <span className="text-sm"> + {new Intl.DateTimeFormat("ko-KR", { + month: "2-digit", + day: "2-digit", + }).format(new Date(submissionDate))} + </span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 80, + }, + + { + accessorKey: "submissionDeadline", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="마감일" />, + cell: ({ row }) => { + const deadline = row.getValue<Date>("submissionDeadline"); + if (!deadline) return <span className="text-muted-foreground">-</span>; + + const now = new Date(); + const isOverdue = now > deadline; + + return ( + <span className={`text-sm ${isOverdue ? "text-red-600" : ""}`}> + {new Intl.DateTimeFormat("ko-KR", { + month: "2-digit", + day: "2-digit", + }).format(new Date(deadline))} + </span> + ); + }, + size: 80, + }, + ] + }, + + // ═══════════════════════════════════════════════════════════════ + // 평가 점수 + // ═══════════════════════════════════════════════════════════════ + { + header: "평가 점수", + columns: [ + { + accessorKey: "totalScore", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="총점" />, + cell: ({ row }) => { + const score = row.getValue<number>("totalScore"); + return score ? ( + <span className="font-medium">{score.toFixed(1)}</span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 80, + }, + + { + accessorKey: "evaluationGrade", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="등급" />, + cell: ({ row }) => { + const grade = row.getValue<string>("evaluationGrade"); + return grade ? ( + <Badge variant={getGradeBadgeVariant(grade)}>{grade}</Badge> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 60, + }, + + { + accessorKey: "finalScore", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종점수" />, + cell: ({ row }) => { + const finalScore = row.getValue<number>("finalScore"); + return finalScore ? ( + <span className="font-bold text-green-600">{finalScore.toFixed(1)}</span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 90, + }, + + { + accessorKey: "finalGrade", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종등급" />, + cell: ({ row }) => { + const finalGrade = row.getValue<string>("finalGrade"); + return finalGrade ? ( + <Badge variant={getGradeBadgeVariant(finalGrade)} className="bg-green-600"> + {finalGrade} + </Badge> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 90, + }, + ] + }, + + // ═══════════════════════════════════════════════════════════════ + // 진행 현황 + // ═══════════════════════════════════════════════════════════════ + { + header: "진행 현황", + columns: [ + { + accessorKey: "status", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="진행상태" />, + cell: ({ row }) => { + const status = row.getValue<string>("status"); + return ( + <Badge variant={getStatusBadgeVariant(status)}> + {getStatusLabel(status)} + </Badge> + ); + }, + size: 100, + }, + + { + id: "reviewProgress", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="리뷰진행" />, + cell: ({ row }) => { + const stats = row.original.reviewerStats; + if (!stats) return <span className="text-muted-foreground">-</span>; + + return getProgressBadge(stats.completedReviewers, stats.totalReviewers); + }, + size: 120, + }, + + { + accessorKey: "reviewCompletedAt", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="검토완료일" />, + cell: ({ row }) => { + const completedAt = row.getValue<Date>("reviewCompletedAt"); + return completedAt ? ( + <span className="text-sm"> + {new Intl.DateTimeFormat("ko-KR", { + month: "2-digit", + day: "2-digit", + }).format(new Date(completedAt))} + </span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 100, + }, + + { + accessorKey: "finalizedAt", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="확정일" />, + cell: ({ row }) => { + const finalizedAt = row.getValue<Date>("finalizedAt"); + return finalizedAt ? ( + <span className="text-sm font-medium"> + {new Intl.DateTimeFormat("ko-KR", { + month: "2-digit", + day: "2-digit", + }).format(new Date(finalizedAt))} + </span> + ) : ( + <span className="text-muted-foreground">-</span> + ); + }, + size: 80, + }, + ] + }, + + // ░░░ Actions ░░░ + { + id: "actions", + enableHiding: false, + size: 40, + minSize: 40, + cell: ({ row }) => { + return ( + <div className="flex items-center gap-1"> + <Button + variant="ghost" + size="icon" + className="size-8" + onClick={() => setRowAction({ row, type: "view" })} + aria-label="상세보기" + title="상세보기" + > + <Eye className="size-4" /> + </Button> + + <Button + variant="ghost" + size="icon" + className="size-8" + onClick={() => setRowAction({ row, type: "update" })} + aria-label="수정" + title="수정" + > + <Pencil className="size-4" /> + </Button> + </div> + ); + }, + }, + ]; +}
\ No newline at end of file diff --git a/lib/evaluation/table/evaluation-filter-sheet.tsx b/lib/evaluation/table/evaluation-filter-sheet.tsx new file mode 100644 index 00000000..7cda4989 --- /dev/null +++ b/lib/evaluation/table/evaluation-filter-sheet.tsx @@ -0,0 +1,1031 @@ +// ================================================================ +// 2. PERIODIC EVALUATIONS FILTER SHEET +// ================================================================ + +"use client" + +import { useEffect, useTransition, useState, useRef } from "react" +import { useRouter, useParams } from "next/navigation" +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Search, X } from "lucide-react" +import { customAlphabet } from "nanoid" +import { parseAsStringEnum, useQueryState } from "nuqs" + +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Badge } from "@/components/ui/badge" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { cn } from "@/lib/utils" +import { getFiltersStateParser } from "@/lib/parsers" + +// nanoid 생성기 +const generateId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6) + +// 정기평가 필터 스키마 정의 +const periodicEvaluationFilterSchema = z.object({ + evaluationYear: z.string().optional(), + evaluationPeriod: z.string().optional(), + division: z.string().optional(), + status: z.string().optional(), + domesticForeign: z.string().optional(), + materialType: z.string().optional(), + vendorCode: z.string().optional(), + vendorName: z.string().optional(), + documentsSubmitted: z.string().optional(), + evaluationGrade: z.string().optional(), + finalGrade: z.string().optional(), + minTotalScore: z.string().optional(), + maxTotalScore: z.string().optional(), +}) + +// 옵션 정의 +const evaluationPeriodOptions = [ + { value: "상반기", label: "상반기" }, + { value: "하반기", label: "하반기" }, + { value: "연간", label: "연간" }, +] + +const divisionOptions = [ + { value: "PLANT", label: "해양" }, + { value: "SHIP", label: "조선" }, +] + +const statusOptions = [ + { value: "PENDING_SUBMISSION", label: "제출대기" }, + { value: "SUBMITTED", label: "제출완료" }, + { value: "IN_REVIEW", label: "검토중" }, + { value: "REVIEW_COMPLETED", label: "검토완료" }, + { value: "FINALIZED", label: "최종확정" }, +] + +const domesticForeignOptions = [ + { value: "DOMESTIC", label: "내자" }, + { value: "FOREIGN", label: "외자" }, +] + +const materialTypeOptions = [ + { value: "EQUIPMENT", label: "기자재" }, + { value: "BULK", label: "벌크" }, + { value: "EQUIPMENT_BULK", label: "기자재/벌크" }, +] + +const documentsSubmittedOptions = [ + { value: "true", label: "제출완료" }, + { value: "false", label: "미제출" }, +] + +const gradeOptions = [ + { value: "S", label: "S등급" }, + { value: "A", label: "A등급" }, + { value: "B", label: "B등급" }, + { value: "C", label: "C등급" }, + { value: "D", label: "D등급" }, +] + +type PeriodicEvaluationFilterFormValues = z.infer<typeof periodicEvaluationFilterSchema> + +interface PeriodicEvaluationFilterSheetProps { + isOpen: boolean; + onClose: () => void; + onSearch?: () => void; + isLoading?: boolean; +} + +export function PeriodicEvaluationFilterSheet({ + isOpen, + onClose, + onSearch, + isLoading = false +}: PeriodicEvaluationFilterSheetProps) { + const router = useRouter() + const params = useParams(); + + const [isPending, startTransition] = useTransition() + const [isInitializing, setIsInitializing] = useState(false) + const lastAppliedFilters = useRef<string>("") + + // nuqs로 URL 상태 관리 + const [filters, setFilters] = useQueryState( + "basicFilters", + getFiltersStateParser().withDefault([]) + ) + + const [joinOperator, setJoinOperator] = useQueryState( + "basicJoinOperator", + parseAsStringEnum(["and", "or"]).withDefault("and") + ) + + // 폼 상태 초기화 + const form = useForm<PeriodicEvaluationFilterFormValues>({ + resolver: zodResolver(periodicEvaluationFilterSchema), + defaultValues: { + evaluationYear: new Date().getFullYear().toString(), + evaluationPeriod: "", + division: "", + status: "", + domesticForeign: "", + materialType: "", + vendorCode: "", + vendorName: "", + documentsSubmitted: "", + evaluationGrade: "", + finalGrade: "", + minTotalScore: "", + maxTotalScore: "", + }, + }) + + // URL 필터에서 초기 폼 상태 설정 + useEffect(() => { + const currentFiltersString = JSON.stringify(filters); + + if (isOpen && filters && filters.length > 0 && currentFiltersString !== lastAppliedFilters.current) { + setIsInitializing(true); + + const formValues = { ...form.getValues() }; + let formUpdated = false; + + filters.forEach(filter => { + if (filter.id in formValues) { + // @ts-ignore - 동적 필드 접근 + formValues[filter.id] = filter.value; + formUpdated = true; + } + }); + + if (formUpdated) { + form.reset(formValues); + lastAppliedFilters.current = currentFiltersString; + } + + setIsInitializing(false); + } + }, [filters, isOpen]) + + // 현재 적용된 필터 카운트 + const getActiveFilterCount = () => { + return filters?.length || 0 + } + + // 폼 제출 핸들러 + async function onSubmit(data: PeriodicEvaluationFilterFormValues) { + if (isInitializing) return; + + startTransition(async () => { + try { + const newFilters = [] + + if (data.evaluationYear?.trim()) { + newFilters.push({ + id: "evaluationYear", + value: parseInt(data.evaluationYear.trim()), + type: "number", + operator: "eq", + rowId: generateId() + }) + } + + if (data.evaluationPeriod?.trim()) { + newFilters.push({ + id: "evaluationPeriod", + value: data.evaluationPeriod.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.division?.trim()) { + newFilters.push({ + id: "division", + value: data.division.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.status?.trim()) { + newFilters.push({ + id: "status", + value: data.status.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.domesticForeign?.trim()) { + newFilters.push({ + id: "domesticForeign", + value: data.domesticForeign.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.materialType?.trim()) { + newFilters.push({ + id: "materialType", + value: data.materialType.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.vendorCode?.trim()) { + newFilters.push({ + id: "vendorCode", + value: data.vendorCode.trim(), + type: "text", + operator: "iLike", + rowId: generateId() + }) + } + + if (data.vendorName?.trim()) { + newFilters.push({ + id: "vendorName", + value: data.vendorName.trim(), + type: "text", + operator: "iLike", + rowId: generateId() + }) + } + + if (data.documentsSubmitted?.trim()) { + newFilters.push({ + id: "documentsSubmitted", + value: data.documentsSubmitted.trim() === "true", + type: "boolean", + operator: "eq", + rowId: generateId() + }) + } + + if (data.evaluationGrade?.trim()) { + newFilters.push({ + id: "evaluationGrade", + value: data.evaluationGrade.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.finalGrade?.trim()) { + newFilters.push({ + id: "finalGrade", + value: data.finalGrade.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.minTotalScore?.trim()) { + newFilters.push({ + id: "totalScore", + value: parseFloat(data.minTotalScore.trim()), + type: "number", + operator: "gte", + rowId: generateId() + }) + } + + if (data.maxTotalScore?.trim()) { + newFilters.push({ + id: "totalScore", + value: parseFloat(data.maxTotalScore.trim()), + type: "number", + operator: "lte", + rowId: generateId() + }) + } + + // URL 업데이트 + const currentUrl = new URL(window.location.href); + const params = new URLSearchParams(currentUrl.search); + + params.delete('basicFilters'); + params.delete('basicJoinOperator'); + params.delete('page'); + + if (newFilters.length > 0) { + params.set('basicFilters', JSON.stringify(newFilters)); + params.set('basicJoinOperator', joinOperator); + } + + params.set('page', '1'); + + const newUrl = `${currentUrl.pathname}?${params.toString()}`; + window.location.href = newUrl; + + lastAppliedFilters.current = JSON.stringify(newFilters); + + if (onSearch) { + onSearch(); + } + } catch (error) { + console.error("정기평가 필터 적용 오류:", error); + } + }) + } + + // 필터 초기화 핸들러 + async function handleReset() { + try { + setIsInitializing(true); + + form.reset({ + evaluationYear: new Date().getFullYear().toString(), + evaluationPeriod: "", + division: "", + status: "", + domesticForeign: "", + materialType: "", + vendorCode: "", + vendorName: "", + documentsSubmitted: "", + evaluationGrade: "", + finalGrade: "", + minTotalScore: "", + maxTotalScore: "", + }); + + const currentUrl = new URL(window.location.href); + const params = new URLSearchParams(currentUrl.search); + + params.delete('basicFilters'); + params.delete('basicJoinOperator'); + params.set('page', '1'); + + const newUrl = `${currentUrl.pathname}?${params.toString()}`; + window.location.href = newUrl; + + lastAppliedFilters.current = ""; + setIsInitializing(false); + } catch (error) { + console.error("정기평가 필터 초기화 오류:", error); + setIsInitializing(false); + } + } + + if (!isOpen) { + return null; + } + + return ( + <div className="flex flex-col h-full max-h-full bg-[#F5F7FB] px-6 sm:px-8"> + {/* Filter Panel Header */} + <div className="flex items-center justify-between px-6 min-h-[60px] shrink-0"> + <h3 className="text-lg font-semibold whitespace-nowrap">정기평가 검색 필터</h3> + <div className="flex items-center gap-2"> + {getActiveFilterCount() > 0 && ( + <Badge variant="secondary" className="px-2 py-1"> + {getActiveFilterCount()}개 필터 적용됨 + </Badge> + )} + </div> + </div> + + {/* Join Operator Selection */} + <div className="px-6 shrink-0"> + <label className="text-sm font-medium">조건 결합 방식</label> + <Select + value={joinOperator} + onValueChange={(value: "and" | "or") => setJoinOperator(value)} + disabled={isInitializing} + > + <SelectTrigger className="h-8 w-[180px] mt-2 bg-white"> + <SelectValue placeholder="조건 결합 방식" /> + </SelectTrigger> + <SelectContent> + <SelectItem value="and">모든 조건 충족 (AND)</SelectItem> + <SelectItem value="or">하나라도 충족 (OR)</SelectItem> + </SelectContent> + </Select> + </div> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col h-full min-h-0"> + {/* Scrollable content area */} + <div className="flex-1 min-h-0 overflow-y-auto px-6 pb-4"> + <div className="space-y-4 pt-2"> + + {/* 평가년도 */} + <FormField + control={form.control} + name="evaluationYear" + render={({ field }) => ( + <FormItem> + <FormLabel>평가년도</FormLabel> + <FormControl> + <div className="relative"> + <Input + type="number" + placeholder="평가년도 입력" + {...field} + className={cn(field.value && "pr-8", "bg-white")} + disabled={isInitializing} + /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="absolute right-0 top-0 h-full px-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("evaluationYear", ""); + }} + disabled={isInitializing} + > + <X className="size-3.5" /> + </Button> + )} + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 평가기간 */} + <FormField + control={form.control} + name="evaluationPeriod" + render={({ field }) => ( + <FormItem> + <FormLabel>평가기간</FormLabel> + <Select + value={field.value} + onValueChange={field.onChange} + disabled={isInitializing} + > + <FormControl> + <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}> + <div className="flex justify-between w-full"> + <SelectValue placeholder="평가기간 선택" /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="h-4 w-4 -mr-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("evaluationPeriod", ""); + }} + disabled={isInitializing} + > + <X className="size-3" /> + </Button> + )} + </div> + </SelectTrigger> + </FormControl> + <SelectContent> + {evaluationPeriodOptions.map(option => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* 구분 */} + <FormField + control={form.control} + name="division" + render={({ field }) => ( + <FormItem> + <FormLabel>구분</FormLabel> + <Select + value={field.value} + onValueChange={field.onChange} + disabled={isInitializing} + > + <FormControl> + <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}> + <div className="flex justify-between w-full"> + <SelectValue placeholder="구분 선택" /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="h-4 w-4 -mr-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("division", ""); + }} + disabled={isInitializing} + > + <X className="size-3" /> + </Button> + )} + </div> + </SelectTrigger> + </FormControl> + <SelectContent> + {divisionOptions.map(option => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* 진행상태 */} + <FormField + control={form.control} + name="status" + render={({ field }) => ( + <FormItem> + <FormLabel>진행상태</FormLabel> + <Select + value={field.value} + onValueChange={field.onChange} + disabled={isInitializing} + > + <FormControl> + <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}> + <div className="flex justify-between w-full"> + <SelectValue placeholder="진행상태 선택" /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="h-4 w-4 -mr-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("status", ""); + }} + disabled={isInitializing} + > + <X className="size-3" /> + </Button> + )} + </div> + </SelectTrigger> + </FormControl> + <SelectContent> + {statusOptions.map(option => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* 내외자 구분 */} + <FormField + control={form.control} + name="domesticForeign" + render={({ field }) => ( + <FormItem> + <FormLabel>내외자 구분</FormLabel> + <Select + value={field.value} + onValueChange={field.onChange} + disabled={isInitializing} + > + <FormControl> + <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}> + <div className="flex justify-between w-full"> + <SelectValue placeholder="내외자 구분 선택" /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="h-4 w-4 -mr-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("domesticForeign", ""); + }} + disabled={isInitializing} + > + <X className="size-3" /> + </Button> + )} + </div> + </SelectTrigger> + </FormControl> + <SelectContent> + {domesticForeignOptions.map(option => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* 자재구분 */} + <FormField + control={form.control} + name="materialType" + render={({ field }) => ( + <FormItem> + <FormLabel>자재구분</FormLabel> + <Select + value={field.value} + onValueChange={field.onChange} + disabled={isInitializing} + > + <FormControl> + <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}> + <div className="flex justify-between w-full"> + <SelectValue placeholder="자재구분 선택" /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="h-4 w-4 -mr-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("materialType", ""); + }} + disabled={isInitializing} + > + <X className="size-3" /> + </Button> + )} + </div> + </SelectTrigger> + </FormControl> + <SelectContent> + {materialTypeOptions.map(option => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* 벤더 코드 */} + <FormField + control={form.control} + name="vendorCode" + render={({ field }) => ( + <FormItem> + <FormLabel>벤더 코드</FormLabel> + <FormControl> + <div className="relative"> + <Input + placeholder="벤더 코드 입력" + {...field} + className={cn(field.value && "pr-8", "bg-white")} + disabled={isInitializing} + /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="absolute right-0 top-0 h-full px-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("vendorCode", ""); + }} + disabled={isInitializing} + > + <X className="size-3.5" /> + </Button> + )} + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 벤더명 */} + <FormField + control={form.control} + name="vendorName" + render={({ field }) => ( + <FormItem> + <FormLabel>벤더명</FormLabel> + <FormControl> + <div className="relative"> + <Input + placeholder="벤더명 입력" + {...field} + className={cn(field.value && "pr-8", "bg-white")} + disabled={isInitializing} + /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="absolute right-0 top-0 h-full px-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("vendorName", ""); + }} + disabled={isInitializing} + > + <X className="size-3.5" /> + </Button> + )} + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 문서제출여부 */} + <FormField + control={form.control} + name="documentsSubmitted" + render={({ field }) => ( + <FormItem> + <FormLabel>문서제출여부</FormLabel> + <Select + value={field.value} + onValueChange={field.onChange} + disabled={isInitializing} + > + <FormControl> + <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}> + <div className="flex justify-between w-full"> + <SelectValue placeholder="문서제출여부 선택" /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="h-4 w-4 -mr-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("documentsSubmitted", ""); + }} + disabled={isInitializing} + > + <X className="size-3" /> + </Button> + )} + </div> + </SelectTrigger> + </FormControl> + <SelectContent> + {documentsSubmittedOptions.map(option => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* 평가등급 */} + <FormField + control={form.control} + name="evaluationGrade" + render={({ field }) => ( + <FormItem> + <FormLabel>평가등급</FormLabel> + <Select + value={field.value} + onValueChange={field.onChange} + disabled={isInitializing} + > + <FormControl> + <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}> + <div className="flex justify-between w-full"> + <SelectValue placeholder="평가등급 선택" /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="h-4 w-4 -mr-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("evaluationGrade", ""); + }} + disabled={isInitializing} + > + <X className="size-3" /> + </Button> + )} + </div> + </SelectTrigger> + </FormControl> + <SelectContent> + {gradeOptions.map(option => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* 최종등급 */} + <FormField + control={form.control} + name="finalGrade" + render={({ field }) => ( + <FormItem> + <FormLabel>최종등급</FormLabel> + <Select + value={field.value} + onValueChange={field.onChange} + disabled={isInitializing} + > + <FormControl> + <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}> + <div className="flex justify-between w-full"> + <SelectValue placeholder="최종등급 선택" /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="h-4 w-4 -mr-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("finalGrade", ""); + }} + disabled={isInitializing} + > + <X className="size-3" /> + </Button> + )} + </div> + </SelectTrigger> + </FormControl> + <SelectContent> + {gradeOptions.map(option => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* 점수 범위 */} + <div className="grid grid-cols-2 gap-2"> + <FormField + control={form.control} + name="minTotalScore" + render={({ field }) => ( + <FormItem> + <FormLabel>최소점수</FormLabel> + <FormControl> + <div className="relative"> + <Input + type="number" + step="0.1" + placeholder="최소" + {...field} + className={cn(field.value && "pr-8", "bg-white")} + disabled={isInitializing} + /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="absolute right-0 top-0 h-full px-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("minTotalScore", ""); + }} + disabled={isInitializing} + > + <X className="size-3.5" /> + </Button> + )} + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="maxTotalScore" + render={({ field }) => ( + <FormItem> + <FormLabel>최대점수</FormLabel> + <FormControl> + <div className="relative"> + <Input + type="number" + step="0.1" + placeholder="최대" + {...field} + className={cn(field.value && "pr-8", "bg-white")} + disabled={isInitializing} + /> + {field.value && ( + <Button + type="button" + variant="ghost" + size="icon" + className="absolute right-0 top-0 h-full px-2" + onClick={(e) => { + e.stopPropagation(); + form.setValue("maxTotalScore", ""); + }} + disabled={isInitializing} + > + <X className="size-3.5" /> + </Button> + )} + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + + </div> + </div> + + {/* Fixed buttons at bottom */} + <div className="p-4 shrink-0"> + <div className="flex gap-2 justify-end"> + <Button + type="button" + variant="outline" + onClick={handleReset} + disabled={isPending || getActiveFilterCount() === 0 || isInitializing} + className="px-4" + > + 초기화 + </Button> + <Button + type="submit" + variant="samsung" + disabled={isPending || isLoading || isInitializing} + className="px-4" + > + <Search className="size-4 mr-2" /> + {isPending || isLoading ? "조회 중..." : "조회"} + </Button> + </div> + </div> + </form> + </Form> + </div> + ) +}
\ No newline at end of file diff --git a/lib/evaluation/table/evaluation-table.tsx b/lib/evaluation/table/evaluation-table.tsx new file mode 100644 index 00000000..16f70592 --- /dev/null +++ b/lib/evaluation/table/evaluation-table.tsx @@ -0,0 +1,462 @@ +"use client" + +import * as React from "react" +import { useRouter, useSearchParams } from "next/navigation" +import { Button } from "@/components/ui/button" +import { PanelLeftClose, PanelLeftOpen } from "lucide-react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Skeleton } from "@/components/ui/skeleton" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { cn } from "@/lib/utils" +import { useTablePresets } from "@/components/data-table/use-table-presets" +import { TablePresetManager } from "@/components/data-table/data-table-preset" +import { useMemo } from "react" +import { PeriodicEvaluationFilterSheet } from "./evaluation-filter-sheet" +import { getPeriodicEvaluationsColumns } from "./evaluation-columns" +import { PeriodicEvaluationView } from "@/db/schema" + +interface PeriodicEvaluationsTableProps { + promises: Promise<[Awaited<ReturnType<typeof getPeriodicEvaluations>>]> + evaluationYear: number + className?: string +} + +// 통계 카드 컴포넌트 +function PeriodicEvaluationsStats({ evaluationYear }: { evaluationYear: number }) { + const [stats, setStats] = React.useState<any>(null) + const [isLoading, setIsLoading] = React.useState(true) + const [error, setError] = React.useState<string | null>(null) + + React.useEffect(() => { + let isMounted = true + + async function fetchStats() { + try { + setIsLoading(true) + setError(null) + // TODO: getPeriodicEvaluationsStats 구현 필요 + const statsData = { + total: 150, + pendingSubmission: 25, + submitted: 45, + inReview: 30, + reviewCompleted: 35, + finalized: 15, + averageScore: 82.5, + completionRate: 75 + } + + if (isMounted) { + setStats(statsData) + } + } catch (err) { + if (isMounted) { + setError(err instanceof Error ? err.message : 'Failed to fetch stats') + console.error('Error fetching periodic evaluations stats:', err) + } + } finally { + if (isMounted) { + setIsLoading(false) + } + } + } + + fetchStats() + + return () => { + isMounted = false + } + }, []) + + if (isLoading) { + return ( + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6"> + {Array.from({ length: 4 }).map((_, i) => ( + <Card key={i}> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <Skeleton className="h-4 w-20" /> + </CardHeader> + <CardContent> + <Skeleton className="h-8 w-16" /> + </CardContent> + </Card> + ))} + </div> + ) + } + + if (error || !stats) { + return ( + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6"> + <Card className="col-span-full"> + <CardContent className="pt-6"> + <div className="text-center text-sm text-muted-foreground"> + {error ? `통계 데이터를 불러올 수 없습니다: ${error}` : "통계 데이터가 없습니다."} + </div> + </CardContent> + </Card> + </div> + ) + } + + const totalEvaluations = stats.total || 0 + const pendingSubmission = stats.pendingSubmission || 0 + const inProgress = (stats.submitted || 0) + (stats.inReview || 0) + (stats.reviewCompleted || 0) + const finalized = stats.finalized || 0 + const completionRate = stats.completionRate || 0 + + return ( + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6"> + {/* 총 평가 */} + <Card> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium">총 평가</CardTitle> + <Badge variant="outline">{evaluationYear}년</Badge> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{totalEvaluations.toLocaleString()}</div> + <div className="text-xs text-muted-foreground mt-1"> + 평균점수 {stats.averageScore?.toFixed(1) || 0}점 + </div> + </CardContent> + </Card> + + {/* 제출대기 */} + <Card> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium">제출대기</CardTitle> + <Badge variant="outline">대기</Badge> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold text-orange-600">{pendingSubmission.toLocaleString()}</div> + <div className="text-xs text-muted-foreground mt-1"> + {totalEvaluations > 0 ? Math.round((pendingSubmission / totalEvaluations) * 100) : 0}% of total + </div> + </CardContent> + </Card> + + {/* 진행중 */} + <Card> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium">진행중</CardTitle> + <Badge variant="secondary">진행</Badge> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold text-blue-600">{inProgress.toLocaleString()}</div> + <div className="text-xs text-muted-foreground mt-1"> + {totalEvaluations > 0 ? Math.round((inProgress / totalEvaluations) * 100) : 0}% of total + </div> + </CardContent> + </Card> + + {/* 완료율 */} + <Card> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium">완료율</CardTitle> + <Badge variant={completionRate >= 80 ? "default" : completionRate >= 60 ? "secondary" : "destructive"}> + {completionRate}% + </Badge> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold text-green-600">{finalized.toLocaleString()}</div> + <div className="text-xs text-muted-foreground mt-1"> + 최종확정 완료 + </div> + </CardContent> + </Card> + </div> + ) +} + +export function PeriodicEvaluationsTable({ promises, evaluationYear, className }: PeriodicEvaluationsTableProps) { + const [rowAction, setRowAction] = React.useState<DataTableRowAction<PeriodicEvaluationView> | null>(null) + const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false) + const router = useRouter() + const searchParams = useSearchParams() + + const containerRef = React.useRef<HTMLDivElement>(null) + const [containerTop, setContainerTop] = React.useState(0) + + const updateContainerBounds = React.useCallback(() => { + if (containerRef.current) { + const rect = containerRef.current.getBoundingClientRect() + const newTop = rect.top + + setContainerTop(prevTop => { + if (Math.abs(prevTop - newTop) > 1) { + return newTop + } + return prevTop + }) + } + }, []) + + const throttledUpdateBounds = React.useCallback(() => { + let timeoutId: NodeJS.Timeout + return () => { + clearTimeout(timeoutId) + timeoutId = setTimeout(updateContainerBounds, 16) + } + }, [updateContainerBounds]) + + React.useEffect(() => { + updateContainerBounds() + + const throttledHandler = throttledUpdateBounds() + + const handleResize = () => { + updateContainerBounds() + } + + window.addEventListener('resize', handleResize) + window.addEventListener('scroll', throttledHandler) + + return () => { + window.removeEventListener('resize', handleResize) + window.removeEventListener('scroll', throttledHandler) + } + }, [updateContainerBounds, throttledUpdateBounds]) + + const [promiseData] = React.use(promises) + const tableData = promiseData + + const initialSettings = React.useMemo(() => ({ + page: parseInt(searchParams.get('page') || '1'), + perPage: parseInt(searchParams.get('perPage') || '10'), + sort: searchParams.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "createdAt", desc: true }], + filters: searchParams.get('filters') ? JSON.parse(searchParams.get('filters')!) : [], + joinOperator: (searchParams.get('joinOperator') as "and" | "or") || "and", + basicFilters: searchParams.get('basicFilters') ? + JSON.parse(searchParams.get('basicFilters')!) : [], + basicJoinOperator: (searchParams.get('basicJoinOperator') as "and" | "or") || "and", + search: searchParams.get('search') || '', + columnVisibility: {}, + columnOrder: [], + pinnedColumns: { left: [], right: ["actions"] }, + groupBy: [], + expandedRows: [] + }), [searchParams]) + + const { + presets, + activePresetId, + hasUnsavedChanges, + isLoading: presetsLoading, + createPreset, + applyPreset, + updatePreset, + deletePreset, + setDefaultPreset, + renamePreset, + updateClientState, + getCurrentSettings, + } = useTablePresets<PeriodicEvaluationView>('periodic-evaluations-table', initialSettings) + + const columns = React.useMemo( + () => getPeriodicEvaluationsColumns({ setRowAction }), + [setRowAction] + ) + + const filterFields: DataTableFilterField<PeriodicEvaluationView>[] = [ + { id: "evaluationTarget.vendorCode", label: "벤더 코드" }, + { id: "evaluationTarget.vendorName", label: "벤더명" }, + { id: "status", label: "진행상태" }, + ] + + const advancedFilterFields: DataTableAdvancedFilterField<PeriodicEvaluationView>[] = [ + { id: "evaluationTarget.evaluationYear", label: "평가년도", type: "number" }, + { id: "evaluationPeriod", label: "평가기간", type: "text" }, + { + id: "evaluationTarget.division", label: "구분", type: "select", options: [ + { label: "해양", value: "PLANT" }, + { label: "조선", value: "SHIP" }, + ] + }, + { id: "evaluationTarget.vendorCode", label: "벤더 코드", type: "text" }, + { id: "evaluationTarget.vendorName", label: "벤더명", type: "text" }, + { + id: "status", label: "진행상태", type: "select", options: [ + { label: "제출대기", value: "PENDING_SUBMISSION" }, + { label: "제출완료", value: "SUBMITTED" }, + { label: "검토중", value: "IN_REVIEW" }, + { label: "검토완료", value: "REVIEW_COMPLETED" }, + { label: "최종확정", value: "FINALIZED" }, + ] + }, + { + id: "documentsSubmitted", label: "문서제출", type: "select", options: [ + { label: "제출완료", value: "true" }, + { label: "미제출", value: "false" }, + ] + }, + { id: "totalScore", label: "총점", type: "number" }, + { id: "finalScore", label: "최종점수", type: "number" }, + { id: "submissionDate", label: "제출일", type: "date" }, + { id: "reviewCompletedAt", label: "검토완료일", type: "date" }, + { id: "finalizedAt", label: "최종확정일", type: "date" }, + ] + + const currentSettings = useMemo(() => { + return getCurrentSettings() + }, [getCurrentSettings]) + + function getColKey<T>(c: ColumnDef<T>): string | undefined { + if ("accessorKey" in c && c.accessorKey) return c.accessorKey as string + if ("id" in c && c.id) return c.id as string + return undefined + } + + const initialState = useMemo(() => { + return { + sorting: initialSettings.sort.filter(s => + columns.some(c => getColKey(c) === s.id)), + columnVisibility: currentSettings.columnVisibility, + columnPinning: currentSettings.pinnedColumns, + } + }, [columns, currentSettings, initialSettings.sort]) + + const { table } = useDataTable({ + data: tableData.data, + columns, + pageCount: tableData.pageCount, + rowCount: tableData.total || tableData.data.length, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + const handleSearch = () => { + setIsFilterPanelOpen(false) + } + + const getActiveBasicFilterCount = () => { + try { + const basicFilters = searchParams.get('basicFilters') + return basicFilters ? JSON.parse(basicFilters).length : 0 + } catch (e) { + return 0 + } + } + + const FILTER_PANEL_WIDTH = 400; + + return ( + <> + {/* Filter Panel */} + <div + className={cn( + "fixed left-0 bg-background border-r z-50 flex flex-col transition-all duration-300 ease-in-out overflow-hidden", + isFilterPanelOpen ? "border-r shadow-lg" : "border-r-0" + )} + style={{ + width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px', + top: `${containerTop}px`, + height: `calc(100vh - ${containerTop}px)` + }} + > + <div className="h-full"> + <PeriodicEvaluationFilterSheet + isOpen={isFilterPanelOpen} + onClose={() => setIsFilterPanelOpen(false)} + onSearch={handleSearch} + isLoading={false} + /> + </div> + </div> + + {/* Main Content Container */} + <div + ref={containerRef} + className={cn("relative w-full overflow-hidden", className)} + > + <div className="flex w-full h-full"> + <div + className="flex flex-col min-w-0 overflow-hidden transition-all duration-300 ease-in-out" + style={{ + width: isFilterPanelOpen ? `calc(100% - ${FILTER_PANEL_WIDTH}px)` : '100%', + marginLeft: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px' + }} + > + {/* Header Bar */} + <div className="flex items-center justify-between p-4 bg-background shrink-0"> + <div className="flex items-center gap-3"> + <Button + variant="outline" + size="sm" + type='button' + onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)} + className="flex items-center shadow-sm" + > + {isFilterPanelOpen ? <PanelLeftClose className="size-4" /> : <PanelLeftOpen className="size-4" />} + {getActiveBasicFilterCount() > 0 && ( + <span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs"> + {getActiveBasicFilterCount()} + </span> + )} + </Button> + </div> + + <div className="text-sm text-muted-foreground"> + {tableData && ( + <span>총 {tableData.total || tableData.data.length}건</span> + )} + </div> + </div> + + {/* 통계 카드들 */} + <div className="px-4"> + <PeriodicEvaluationsStats evaluationYear={evaluationYear} /> + </div> + + {/* Table Content Area */} + <div className="flex-1 overflow-hidden" style={{ height: 'calc(100vh - 500px)' }}> + <div className="h-full w-full"> + <DataTable table={table} className="h-full"> + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <div className="flex items-center gap-2"> + <TablePresetManager<PeriodicEvaluationView> + presets={presets} + activePresetId={activePresetId} + currentSettings={currentSettings} + hasUnsavedChanges={hasUnsavedChanges} + isLoading={presetsLoading} + onCreatePreset={createPreset} + onUpdatePreset={updatePreset} + onDeletePreset={deletePreset} + onApplyPreset={applyPreset} + onSetDefaultPreset={setDefaultPreset} + onRenamePreset={renamePreset} + /> + + {/* TODO: PeriodicEvaluationsTableToolbarActions 구현 */} + </div> + </DataTableAdvancedToolbar> + </DataTable> + + {/* TODO: 수정/상세보기 모달 구현 */} + + </div> + </div> + </div> + </div> + </div> + </> + ) +}
\ No newline at end of file diff --git a/lib/evaluation/validation.ts b/lib/evaluation/validation.ts new file mode 100644 index 00000000..9179f585 --- /dev/null +++ b/lib/evaluation/validation.ts @@ -0,0 +1,46 @@ +import { + createSearchParamsCache, + parseAsArrayOf, + parseAsInteger, + parseAsString, + parseAsStringEnum, + } from "nuqs/server"; + import * as z from "zod"; + + import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"; +import { periodicEvaluations } from "@/db/schema"; + + // ============= 메인 검색 파라미터 스키마 ============= + + export const searchParamsEvaluationsCache = createSearchParamsCache({ + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + sort: getSortingStateParser<typeof periodicEvaluations>().withDefault([ + { id: "createdAt", desc: true }]), + + // 기본 필터들 + evaluationYear: parseAsInteger.withDefault(new Date().getFullYear()), + division: parseAsString.withDefault(""), + status: parseAsString.withDefault(""), + domesticForeign: parseAsString.withDefault(""), + materialType: parseAsString.withDefault(""), + consensusStatus: parseAsString.withDefault(""), + + // 고급 필터 + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + + // 베이직 필터 (커스텀 필터 패널용) + basicFilters: getFiltersStateParser().withDefault([]), + basicJoinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + + // 검색 + search: parseAsString.withDefault(""), + }); + + // ============= 타입 정의 ============= + + export type GetEvaluationsSchema = Awaited< + ReturnType<typeof searchParamsEvaluationsCache.parse> + >; diff --git a/lib/forms/services.ts b/lib/forms/services.ts index 021bb767..0558e83f 100644 --- a/lib/forms/services.ts +++ b/lib/forms/services.ts @@ -27,6 +27,7 @@ import { getErrorMessage } from "../handle-error"; import { DataTableColumnJSON } from "@/components/form-data/form-data-table-columns"; import { contractItems, contracts, items, projects } from "@/db/schema"; import { getSEDPToken } from "../sedp/sedp-token"; +import { decryptWithServerAction } from "@/components/drm/drmUtils"; export type FormInfo = InferSelectModel<typeof forms>; @@ -47,9 +48,9 @@ export async function getFormsByContractItemId( try { // return unstable_cache( // async () => { - console.log( - `[Forms Service] Fetching forms for contractItemId: ${contractItemId}, mode: ${mode}` - ); + // console.log( + // `[Forms Service] Fetching forms for contractItemId: ${contractItemId}, mode: ${mode}` + // ); try { // 쿼리 생성 @@ -227,10 +228,9 @@ async function getEditableFieldsByTag( * 그리고 이 로직 전체를 unstable_cache로 감싸 캐싱. */ export async function getFormData(formCode: string, contractItemId: number) { - const cacheKey = `form-data-${formCode}-${contractItemId}`; - console.log(cacheKey, "getFormData") try { + // 기존 로직으로 projectId, columns, data 가져오기 const contractItemResult = await db .select({ @@ -285,6 +285,8 @@ export async function getFormData(formCode: string, contractItemId: number) { const excludeKeys = ['BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO']; columns = columns.filter(col => !excludeKeys.includes(col.key)); + + columns.forEach((col) => { if (!col.displayLabel) { if (col.uom) { @@ -295,25 +297,24 @@ export async function getFormData(formCode: string, contractItemId: number) { } }); - // status 컬럼 추가 columns.push({ - key: "status", - label: "status", - displayLabel: "Status", - type: "STRING" - }); + key:"status", + label:"status", + displayLabel:"Status", + type:"STRING" + }) let data: Array<Record<string, any>> = []; if (entry) { if (Array.isArray(entry.data)) { data = entry.data; - data.sort((a, b) => { + data.sort((a,b) => { const statusA = a.status || ''; const statusB = b.status || ''; - return statusB.localeCompare(statusA); - }); - + return statusB.localeCompare(statusA) + }) + } else { console.warn("formEntries data was not an array. Using empty array."); } @@ -382,7 +383,7 @@ export async function getFormData(formCode: string, contractItemId: number) { const entry = entryRows[0] ?? null; let columns = meta.columns as DataTableColumnJSON[]; - const excludeKeys = ['BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO']; + const excludeKeys = [ 'BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO']; columns = columns.filter(col => !excludeKeys.includes(col.key)); columns.forEach((col) => { @@ -693,7 +694,7 @@ export async function updateFormDataInDB( ...oldItem, ...newData, TAG_NO: oldItem.TAG_NO, // TAG_NO 변경 불가 시 유지 - status: "Imported from EXCEL" // Excel에서 가져온 데이터임을 표시 + status: "Updated" // Excel에서 가져온 데이터임을 표시 }; const updatedArray = [...dataArray]; @@ -894,7 +895,8 @@ export async function uploadReportTemp( const savePath = path.join(baseDir, uniqueName); - const arrayBuffer = await file.arrayBuffer(); + // const arrayBuffer = await file.arrayBuffer(); + const arrayBuffer = await decryptWithServerAction(file); const buffer = Buffer.from(arrayBuffer); await fs.mkdir(baseDir, { recursive: true }); diff --git a/lib/incoterms/table/delete-incoterms-dialog.tsx b/lib/incoterms/table/delete-incoterms-dialog.tsx new file mode 100644 index 00000000..8b91033c --- /dev/null +++ b/lib/incoterms/table/delete-incoterms-dialog.tsx @@ -0,0 +1,154 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, Trash } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" + +import { deleteIncoterm } from "../service" +import { incoterms } from "@/db/schema/procurementRFQ" + +interface DeleteIncotermsDialogProps + extends React.ComponentPropsWithoutRef<typeof Dialog> { + incoterms: Row<typeof incoterms.$inferSelect>["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeleteIncotermsDialog({ + incoterms, + showTrigger = true, + onSuccess, + ...props +}: DeleteIncotermsDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onDelete() { + startDeleteTransition(async () => { + try { + // 각 인코텀즈를 순차적으로 삭제 + for (const incoterm of incoterms) { + const result = await deleteIncoterm(incoterm.code) + if (!result.success) { + toast.error(`인코텀즈 ${incoterm.code} 삭제 실패: ${result.error}`) + return + } + } + + props.onOpenChange?.(false) + toast.success("인코텀즈가 성공적으로 삭제되었습니다.") + onSuccess?.() + } catch (error) { + console.error("Delete error:", error) + toast.error("인코텀즈 삭제 중 오류가 발생했습니다.") + } + }) + } + + if (isDesktop) { + return ( + <Dialog {...props}> + {showTrigger ? ( + <DialogTrigger asChild> + <Button variant="outline" size="sm"> + <Trash className="mr-2 size-4" aria-hidden="true" /> + delete ({incoterms.length}) + </Button> + </DialogTrigger> + ) : null} + <DialogContent> + <DialogHeader> + <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle> + <DialogDescription> + 이 작업은 되돌릴 수 없습니다. 선택된{" "} + <span className="font-medium">{incoterms.length}</span> + 개의 인코텀즈를 서버에서 영구적으로 삭제합니다. + </DialogDescription> + </DialogHeader> + <DialogFooter className="gap-2 sm:space-x-0"> + <DialogClose asChild> + <Button variant="outline">취소</Button> + </DialogClose> + <Button + aria-label="선택된 행 삭제" + variant="destructive" + onClick={onDelete} + disabled={isDeletePending} + > + {isDeletePending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + 삭제 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) + } + + return ( + <Drawer {...props}> + {showTrigger ? ( + <DrawerTrigger asChild> + <Button variant="outline" size="sm"> + <Trash className="mr-2 size-4" aria-hidden="true" /> + delete ({incoterms.length}) + </Button> + </DrawerTrigger> + ) : null} + <DrawerContent> + <DrawerHeader> + <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle> + <DrawerDescription> + 이 작업은 되돌릴 수 없습니다. 선택된{" "} + <span className="font-medium">{incoterms.length}</span> + 개의 인코텀즈를 서버에서 영구적으로 삭제합니다. + </DrawerDescription> + </DrawerHeader> + <DrawerFooter className="gap-2 sm:space-x-0"> + <DrawerClose asChild> + <Button variant="outline">취소</Button> + </DrawerClose> + <Button + aria-label="선택된 행 삭제" + variant="destructive" + onClick={onDelete} + disabled={isDeletePending} + > + {isDeletePending && ( + <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> + )} + delete + </Button> + </DrawerFooter> + </DrawerContent> + </Drawer> + ) +}
\ No newline at end of file diff --git a/lib/incoterms/table/incoterms-add-dialog.tsx b/lib/incoterms/table/incoterms-add-dialog.tsx index ef378e1e..0f7384d6 100644 --- a/lib/incoterms/table/incoterms-add-dialog.tsx +++ b/lib/incoterms/table/incoterms-add-dialog.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; +import * as z from "zod"; import { Plus, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -70,7 +70,8 @@ export function IncotermsAddDialog({ onSuccess }: IncotermsAddDialogProps) { try { const result = await createIncoterm(data); if (result.data) { - toast.success("인코텀즈가 추가되었습니다."); + toast.success("인코텀즈가 성공적으로 추가되었습니다."); + form.reset(); setOpen(false); if (onSuccess) { onSuccess(); @@ -89,16 +90,17 @@ export function IncotermsAddDialog({ onSuccess }: IncotermsAddDialogProps) { return ( <Dialog open={open} onOpenChange={handleOpenChange}> <DialogTrigger asChild> - <Button size="sm" variant="outline"> + <Button variant="outline" size="sm"> <Plus className="mr-2 h-4 w-4" /> 인코텀즈 추가 </Button> </DialogTrigger> <DialogContent className="max-w-md"> <DialogHeader> - <DialogTitle>인코텀즈 추가</DialogTitle> + <DialogTitle>새 인코텀즈 추가</DialogTitle> <DialogDescription> 새로운 인코텀즈를 추가합니다. 필수 정보를 입력해주세요. + <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span> </DialogDescription> </DialogHeader> @@ -153,7 +155,7 @@ export function IncotermsAddDialog({ onSuccess }: IncotermsAddDialogProps) { disabled={isLoading} > {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} - {isLoading ? "생성 중..." : "인코텀즈 추가"} + {isLoading ? "생성 중..." : "추가"} </Button> </DialogFooter> </DialogContent> diff --git a/lib/incoterms/table/incoterms-edit-sheet.tsx b/lib/incoterms/table/incoterms-edit-sheet.tsx index 9cd067c7..1ae6e902 100644 --- a/lib/incoterms/table/incoterms-edit-sheet.tsx +++ b/lib/incoterms/table/incoterms-edit-sheet.tsx @@ -5,6 +5,8 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import { toast } from "sonner" import * as z from "zod" +import { Loader } from "lucide-react" + import { Button } from "@/components/ui/button" import { Form, @@ -16,8 +18,10 @@ import { } from "@/components/ui/form" import { Sheet, + SheetClose, SheetContent, SheetDescription, + SheetFooter, SheetHeader, SheetTitle, } from "@/components/ui/sheet" @@ -37,7 +41,7 @@ type UpdateIncotermSchema = z.infer<typeof updateIncotermSchema> interface IncotermsEditSheetProps { open: boolean onOpenChange: (open: boolean) => void - data: typeof incoterms.$inferSelect + data: typeof incoterms.$inferSelect | null onSuccess: () => void } @@ -47,12 +51,14 @@ export function IncotermsEditSheet({ data, onSuccess, }: IncotermsEditSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + const form = useForm<UpdateIncotermSchema>({ resolver: zodResolver(updateIncotermSchema), defaultValues: { - code: data.code, - description: data.description, - isActive: data.isActive, + code: data?.code ?? "", + description: data?.description ?? "", + isActive: data?.isActive ?? true, }, mode: "onChange" }) @@ -68,14 +74,19 @@ export function IncotermsEditSheet({ }, [data, form]) async function onSubmit(input: UpdateIncotermSchema) { - try { - await updateIncoterm(data.code, input) - toast.success("수정이 완료되었습니다.") - onSuccess() - onOpenChange(false) - } catch { - toast.error("수정 중 오류가 발생했습니다.") - } + if (!data) return + + startUpdateTransition(async () => { + try { + await updateIncoterm(data.code, input) + toast.success("인코텀즈가 성공적으로 수정되었습니다.") + onSuccess() + onOpenChange(false) + } catch (error) { + console.error("Update error:", error) + toast.error("인코텀즈 수정 중 오류가 발생했습니다.") + } + }) } return ( @@ -96,7 +107,7 @@ export function IncotermsEditSheet({ <FormItem> <FormLabel>코드</FormLabel> <FormControl> - <Input {...field} disabled /> + <Input {...field} /> </FormControl> <FormMessage /> </FormItem> @@ -132,12 +143,25 @@ export function IncotermsEditSheet({ </FormItem> )} /> - <div className="flex justify-end space-x-2"> - <Button type="button" variant="outline" onClick={() => onOpenChange(false)}> - 취소 + <SheetFooter className="gap-2 pt-2 sm:space-x-0"> + <SheetClose asChild> + <Button type="button" variant="outline"> + 취소 + </Button> + </SheetClose> + <Button + type="submit" + disabled={isUpdatePending || !form.formState.isValid} + > + {isUpdatePending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + 저장 </Button> - <Button type="submit">저장</Button> - </div> + </SheetFooter> </form> </Form> </SheetContent> diff --git a/lib/incoterms/table/incoterms-table-columns.tsx b/lib/incoterms/table/incoterms-table-columns.tsx index 56a44e8b..91ce4482 100644 --- a/lib/incoterms/table/incoterms-table-columns.tsx +++ b/lib/incoterms/table/incoterms-table-columns.tsx @@ -1,76 +1,71 @@ -import type { ColumnDef, Row } from "@tanstack/react-table"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Ellipsis } from "lucide-react"; +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Ellipsis } from "lucide-react" + +import { formatDateTime } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, + DropdownMenuShortcut, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { incoterms } from "@/db/schema/procurementRFQ"; -import { toast } from "sonner"; -import { deleteIncoterm } from "../service"; +} from "@/components/ui/dropdown-menu" -type Incoterm = typeof incoterms.$inferSelect; +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { incoterms } from "@/db/schema/procurementRFQ" interface GetColumnsProps { - setRowAction: (action: { type: string; row: Row<Incoterm> }) => void; - onSuccess: () => void; + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<typeof incoterms.$inferSelect> | null>> } -const handleDelete = async (code: string, onSuccess: () => void) => { - const result = await deleteIncoterm(code); - if (result.success) { - toast.success("삭제 완료"); - onSuccess(); - } else { - toast.error(result.error || "삭제 실패"); +/** + * tanstack table 컬럼 정의 + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof incoterms.$inferSelect>[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef<typeof incoterms.$inferSelect> = { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + maxSize: 30, + enableSorting: false, + enableHiding: false, } -}; -export function getColumns({ setRowAction, onSuccess }: GetColumnsProps): ColumnDef<Incoterm>[] { - return [ - { - id: "code", - header: () => <div>코드</div>, - cell: ({ row }) => <div>{row.original.code}</div>, - enableSorting: true, - enableHiding: false, - }, - { - id: "description", - header: () => <div>설명</div>, - cell: ({ row }) => <div>{row.original.description}</div>, - enableSorting: true, - enableHiding: false, - }, - { - id: "isActive", - header: () => <div>상태</div>, - cell: ({ row }) => ( - <Badge variant={row.original.isActive ? "default" : "secondary"}> - {row.original.isActive ? "활성" : "비활성"} - </Badge> - ), - enableSorting: true, - enableHiding: false, - }, - { - id: "createdAt", - header: () => <div>생성일</div>, - cell: ({ row }) => { - const value = row.original.createdAt; - const date = value ? new Date(value) : null; - return date ? date.toLocaleDateString() : ""; - }, - enableSorting: true, - enableHiding: false, - }, - { - id: "actions", - cell: ({ row }) => ( + // ---------------------------------------------------------------- + // 2) actions 컬럼 (Dropdown 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef<typeof incoterms.$inferSelect> = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button @@ -83,20 +78,99 @@ export function getColumns({ setRowAction, onSuccess }: GetColumnsProps): Column </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-40"> <DropdownMenuItem - onSelect={() => setRowAction({ type: "edit", row })} + onSelect={() => setRowAction({ row, type: "update" })} > - 수정 + Edit </DropdownMenuItem> + <DropdownMenuSeparator /> <DropdownMenuItem - onSelect={() => handleDelete(row.original.code, onSuccess)} - className="text-destructive" + onSelect={() => setRowAction({ row, type: "delete" })} > - 삭제 + Delete + <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> + ) + }, + maxSize: 30, + } + + // ---------------------------------------------------------------- + // 3) 데이터 컬럼들 + // ---------------------------------------------------------------- + const dataColumns: ColumnDef<typeof incoterms.$inferSelect>[] = [ + { + accessorKey: "code", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="코드" /> + ), + meta: { + excelHeader: "코드", + type: "text", + }, + cell: ({ row }) => row.getValue("code") ?? "", + minSize: 80 + }, + { + accessorKey: "description", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="설명" /> ), + meta: { + excelHeader: "설명", + type: "text", + }, + cell: ({ row }) => row.getValue("description") ?? "", + minSize: 80 }, - ]; + { + accessorKey: "isActive", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="상태" /> + ), + meta: { + excelHeader: "상태", + type: "boolean", + }, + cell: ({ row }) => { + const isActive = row.getValue("isActive") as boolean + return ( + <Badge variant={isActive ? "default" : "secondary"}> + {isActive ? "활성" : "비활성"} + </Badge> + ) + }, + minSize: 80 + }, + { + accessorKey: "createdAt", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="생성일" /> + ), + meta: { + excelHeader: "생성일", + type: "date", + }, + cell: ({ row }) => { + const dateVal = row.getValue("createdAt") as Date + return formatDateTime(dateVal) + }, + minSize: 80 + } + ] + + // ---------------------------------------------------------------- + // 4) 최종 컬럼 배열: select, dataColumns, actions + // ---------------------------------------------------------------- + return [ + selectColumn, + ...dataColumns, + actionsColumn, + ] }
\ No newline at end of file diff --git a/lib/incoterms/table/incoterms-table-toolbar.tsx b/lib/incoterms/table/incoterms-table-toolbar.tsx index b87982c9..698acf59 100644 --- a/lib/incoterms/table/incoterms-table-toolbar.tsx +++ b/lib/incoterms/table/incoterms-table-toolbar.tsx @@ -1,16 +1,53 @@ "use client"; import * as React from "react"; +import { type Table } from "@tanstack/react-table"; +import { Download } from "lucide-react"; + +import { exportTableToExcel } from "@/lib/export"; +import { Button } from "@/components/ui/button"; +import { DeleteIncotermsDialog } from "./delete-incoterms-dialog"; import { IncotermsAddDialog } from "./incoterms-add-dialog"; +import { incoterms } from "@/db/schema/procurementRFQ"; -interface IncotermsTableToolbarProps { +interface IncotermsTableToolbarActionsProps { + table: Table<typeof incoterms.$inferSelect>; onSuccess?: () => void; } -export function IncotermsTableToolbar({ onSuccess }: IncotermsTableToolbarProps) { +export function IncotermsTableToolbarActions({ table, onSuccess }: IncotermsTableToolbarActionsProps) { return ( <div className="flex items-center gap-2"> + {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */} + {table.getFilteredSelectedRowModel().rows.length > 0 ? ( + <DeleteIncotermsDialog + incoterms={table + .getFilteredSelectedRowModel() + .rows.map((row) => row.original)} + onSuccess={() => { + table.toggleAllRowsSelected(false); + onSuccess?.(); + }} + /> + ) : null} + <IncotermsAddDialog onSuccess={onSuccess} /> + + {/** 3) Export 버튼 */} + <Button + variant="outline" + size="sm" + onClick={() => + exportTableToExcel(table, { + filename: "incoterms-list", + excludeColumns: ["select", "actions"], + }) + } + className="gap-2" + > + <Download className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">Export</span> + </Button> </div> ); }
\ No newline at end of file diff --git a/lib/incoterms/table/incoterms-table.tsx b/lib/incoterms/table/incoterms-table.tsx index c5b5bba4..c98de810 100644 --- a/lib/incoterms/table/incoterms-table.tsx +++ b/lib/incoterms/table/incoterms-table.tsx @@ -3,13 +3,16 @@ import * as React from "react"; import { useDataTable } from "@/hooks/use-data-table"; import { DataTable } from "@/components/data-table/data-table"; import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"; +import type { + DataTableAdvancedFilterField, + DataTableRowAction, +} from "@/types/table" +import { getIncoterms } from "../service"; import { getColumns } from "./incoterms-table-columns"; -import { incoterms } from "@/db/schema/procurementRFQ"; -import { IncotermsTableToolbar } from "./incoterms-table-toolbar"; -import { toast } from "sonner"; +import { DeleteIncotermsDialog } from "./delete-incoterms-dialog"; import { IncotermsEditSheet } from "./incoterms-edit-sheet"; -import { Row } from "@tanstack/react-table"; -import { getIncoterms } from "../service"; +import { IncotermsTableToolbarActions } from "./incoterms-table-toolbar"; +import { incoterms } from "@/db/schema/procurementRFQ"; interface IncotermsTableProps { promises?: Promise<[{ data: typeof incoterms.$inferSelect[]; pageCount: number }] >; @@ -17,8 +20,7 @@ interface IncotermsTableProps { export function IncotermsTable({ promises }: IncotermsTableProps) { const [rawData, setRawData] = React.useState<{ data: typeof incoterms.$inferSelect[]; pageCount: number }>({ data: [], pageCount: 0 }); - const [isEditSheetOpen, setIsEditSheetOpen] = React.useState(false); - const [selectedRow, setSelectedRow] = React.useState<typeof incoterms.$inferSelect | null>(null); + const [rowAction, setRowAction] = React.useState<DataTableRowAction<typeof incoterms.$inferSelect> | null>(null); React.useEffect(() => { if (promises) { @@ -44,7 +46,6 @@ export function IncotermsTable({ promises }: IncotermsTableProps) { setRawData(result); } catch (error) { console.error("Error refreshing data:", error); - toast.error("데이터를 불러오는 중 오류가 발생했습니다."); } })(); } @@ -67,50 +68,71 @@ export function IncotermsTable({ promises }: IncotermsTableProps) { setRawData(result); } catch (error) { console.error("Error refreshing data:", error); - toast.error("데이터를 불러오는 중 오류가 발생했습니다."); } }, []); - const handleRowAction = async (action: { type: string; row: Row<typeof incoterms.$inferSelect> }) => { - if (action.type === "edit") { - setSelectedRow(action.row.original); - setIsEditSheetOpen(true); - } - }; + // 컬럼 설정 - 외부 파일에서 가져옴 + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) - const columns = React.useMemo(() => getColumns({ setRowAction: handleRowAction, onSuccess: refreshData }), [refreshData]); + // 고급 필터 필드 설정 + const advancedFilterFields: DataTableAdvancedFilterField<typeof incoterms.$inferSelect>[] = [ + { id: "code", label: "코드", type: "text" }, + { + id: "isActive", label: "상태", type: "select", options: [ + { label: "활성", value: "true" }, + { label: "비활성", value: "false" }, + ] + }, + { id: "description", label: "설명", type: "text" }, + { id: "createdAt", label: "생성일", type: "date" }, + ]; const { table } = useDataTable({ - data: rawData.data, - columns, - pageCount: rawData.pageCount, - filterFields: [], - enablePinning: true, - enableAdvancedFilter: true, - initialState: { - sorting: [{ id: "createdAt", desc: true }], - columnPinning: { right: ["actions"] }, - }, - getRowId: (originalRow) => String(originalRow.code), - shallow: false, - clearOnDefault: true, - }); + data: rawData.data, + columns, + pageCount: rawData.pageCount, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.code), + shallow: false, + clearOnDefault: true, + }) return ( <> <DataTable table={table}> - <DataTableAdvancedToolbar table={table} filterFields={[]} shallow={false}> - <IncotermsTableToolbar onSuccess={refreshData} /> + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + > + <IncotermsTableToolbarActions table={table} onSuccess={refreshData} /> </DataTableAdvancedToolbar> </DataTable> - {isEditSheetOpen && selectedRow && ( - <IncotermsEditSheet - open={isEditSheetOpen} - onOpenChange={setIsEditSheetOpen} - data={selectedRow} - onSuccess={refreshData} - /> - )} + + <DeleteIncotermsDialog + open={rowAction?.type === "delete"} + onOpenChange={() => setRowAction(null)} + incoterms={rowAction?.row.original ? [rowAction?.row.original] : []} + showTrigger={false} + onSuccess={() => { + rowAction?.row.toggleSelected(false) + refreshData() + }} + /> + + <IncotermsEditSheet + open={rowAction?.type === "update"} + onOpenChange={() => setRowAction(null)} + data={rowAction?.row.original ?? null} + onSuccess={refreshData} + /> </> ); }
\ No newline at end of file diff --git a/lib/mail/templates/evaluation-review-request.hbs b/lib/mail/templates/evaluation-review-request.hbs new file mode 100644 index 00000000..022f438b --- /dev/null +++ b/lib/mail/templates/evaluation-review-request.hbs @@ -0,0 +1,162 @@ +<!-- evaluation-review-request.hbs --> +<!DOCTYPE html> +<html lang="ko"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>벤더 평가 의견 요청</title> + <style> + body { + font-family: 'Malgun Gothic', '맑은 고딕', Arial, sans-serif; + line-height: 1.6; + color: #333; + max-width: 600px; + margin: 0 auto; + padding: 20px; + background-color: #f5f5f5; + } + .container { + background-color: #ffffff; + border-radius: 8px; + padding: 30px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + } + .header { + text-align: center; + border-bottom: 2px solid #e9ecef; + padding-bottom: 20px; + margin-bottom: 30px; + } + .header h1 { + color: #2563eb; + font-size: 24px; + margin: 0; + } + .content { + margin-bottom: 30px; + } + .info-box { + background-color: #f8f9fa; + border-left: 4px solid #2563eb; + padding: 15px; + margin: 20px 0; + border-radius: 4px; + } + .target-list { + background-color: #f8f9fa; + border-radius: 6px; + padding: 15px; + margin: 15px 0; + } + .target-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid #e9ecef; + } + .target-item:last-child { + border-bottom: none; + } + .vendor-code { + background-color: #e9ecef; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: bold; + } + .button { + display: inline-block; + background-color: #2563eb; + color: white; + padding: 12px 24px; + text-decoration: none; + border-radius: 6px; + font-weight: bold; + text-align: center; + margin: 20px auto; + } + .button:hover { + background-color: #1d4ed8; + } + .message-box { + background-color: #fef3c7; + border: 1px solid #f59e0b; + border-radius: 6px; + padding: 15px; + margin: 20px 0; + } + .footer { + border-top: 1px solid #e9ecef; + padding-top: 20px; + margin-top: 30px; + text-align: center; + color: #6b7280; + font-size: 14px; + } + </style> +</head> +<body> + <div class="container"> + <div class="header"> + <h1>🔍 벤더 평가 의견 요청</h1> + </div> + + <div class="content"> + <p>안녕하세요{{#if reviewerName}}, <strong>{{reviewerName}}</strong>님{{/if}}</p> + + <p><strong>{{requesterName}}</strong>님이 벤더 평가에 대한 의견을 요청하셨습니다.</p> + + <div class="info-box"> + <p><strong>📋 요청 정보</strong></p> + <ul style="margin: 10px 0;"> + <li>요청 일시: {{requestDate}}</li> + <li>평가 대상: {{targetCount}}개 벤더</li> + </ul> + </div> + + {{#if message}} + <div class="message-box"> + <p><strong>💬 요청자 메시지:</strong></p> + <p style="margin: 8px 0; white-space: pre-line;">{{message}}</p> + </div> + {{/if}} + + <div class="target-list"> + <p><strong>📄 평가 대상 목록:</strong></p> + {{#each targets}} + <div class="target-item"> + <div> + <span class="vendor-code">{{this.vendorCode}}</span> + <span style="margin-left: 8px;">{{this.vendorName}}</span> + </div> + <div style="font-size: 12px; color: #6b7280;"> + {{this.materialType}} ({{this.evaluationYear}}년) + </div> + </div> + {{/each}} + </div> + + <div style="text-align: center;"> + <a href="{{reviewUrl}}" class="button"> + 📝 평가 의견 작성하기 + </a> + </div> + + <div class="info-box"> + <p><strong>💡 참고사항:</strong></p> + <ul style="margin: 10px 0;"> + <li>평가 시스템에 로그인하여 각 벤더에 대한 의견을 입력해주세요.</li> + <li>평가 여부(여/부)와 함께 종합 의견도 작성 가능합니다.</li> + <li>궁금한 사항이 있으시면 요청자에게 직접 문의해주세요.</li> + </ul> + </div> + </div> + + <div class="footer"> + <p>이 메일은 벤더 평가 시스템에서 자동으로 발송되었습니다.</p> + <p>문의사항이 있으시면 시스템 관리자에게 연락해주세요.</p> + </div> + </div> +</body> +</html>
\ No newline at end of file diff --git a/lib/payment-terms/table/delete-payment-terms-dialog.tsx b/lib/payment-terms/table/delete-payment-terms-dialog.tsx new file mode 100644 index 00000000..3e955fce --- /dev/null +++ b/lib/payment-terms/table/delete-payment-terms-dialog.tsx @@ -0,0 +1,154 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, Trash } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" + +import { deletePaymentTerm } from "../service" +import { paymentTerms } from "@/db/schema/procurementRFQ" + +interface DeletePaymentTermsDialogProps + extends React.ComponentPropsWithoutRef<typeof Dialog> { + paymentTerms: Row<typeof paymentTerms.$inferSelect>["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeletePaymentTermsDialog({ + paymentTerms, + showTrigger = true, + onSuccess, + ...props +}: DeletePaymentTermsDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onDelete() { + startDeleteTransition(async () => { + try { + // 각 결제 조건을 순차적으로 삭제 + for (const paymentTerm of paymentTerms) { + const result = await deletePaymentTerm(paymentTerm.code) + if (!result.success) { + toast.error(`결제 조건 ${paymentTerm.code} 삭제 실패: ${result.error}`) + return + } + } + + props.onOpenChange?.(false) + toast.success("결제 조건이 성공적으로 삭제되었습니다.") + onSuccess?.() + } catch (error) { + console.error("Delete error:", error) + toast.error("결제 조건 삭제 중 오류가 발생했습니다.") + } + }) + } + + if (isDesktop) { + return ( + <Dialog {...props}> + {showTrigger ? ( + <DialogTrigger asChild> + <Button variant="outline" size="sm"> + <Trash className="mr-2 size-4" aria-hidden="true" /> + delete ({paymentTerms.length}) + </Button> + </DialogTrigger> + ) : null} + <DialogContent> + <DialogHeader> + <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle> + <DialogDescription> + 이 작업은 되돌릴 수 없습니다. 선택된{" "} + <span className="font-medium">{paymentTerms.length}</span> + 개의 결제 조건을 서버에서 영구적으로 삭제합니다. + </DialogDescription> + </DialogHeader> + <DialogFooter className="gap-2 sm:space-x-0"> + <DialogClose asChild> + <Button variant="outline">취소</Button> + </DialogClose> + <Button + aria-label="선택된 행 삭제" + variant="destructive" + onClick={onDelete} + disabled={isDeletePending} + > + {isDeletePending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + 삭제 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) + } + + return ( + <Drawer {...props}> + {showTrigger ? ( + <DrawerTrigger asChild> + <Button variant="outline" size="sm"> + <Trash className="mr-2 size-4" aria-hidden="true" /> + delete ({paymentTerms.length}) + </Button> + </DrawerTrigger> + ) : null} + <DrawerContent> + <DrawerHeader> + <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle> + <DrawerDescription> + 이 작업은 되돌릴 수 없습니다. 선택된{" "} + <span className="font-medium">{paymentTerms.length}</span> + 개의 결제 조건을 서버에서 영구적으로 삭제합니다. + </DrawerDescription> + </DrawerHeader> + <DrawerFooter className="gap-2 sm:space-x-0"> + <DrawerClose asChild> + <Button variant="outline">취소</Button> + </DrawerClose> + <Button + aria-label="선택된 행 삭제" + variant="destructive" + onClick={onDelete} + disabled={isDeletePending} + > + {isDeletePending && ( + <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> + )} + 삭제 + </Button> + </DrawerFooter> + </DrawerContent> + </Drawer> + ) +}
\ No newline at end of file diff --git a/lib/payment-terms/table/payment-terms-add-dialog.tsx b/lib/payment-terms/table/payment-terms-add-dialog.tsx index 9aa21485..49819f87 100644 --- a/lib/payment-terms/table/payment-terms-add-dialog.tsx +++ b/lib/payment-terms/table/payment-terms-add-dialog.tsx @@ -70,7 +70,8 @@ export function PaymentTermsAddDialog({ onSuccess }: PaymentTermsAddDialogProps) try { const result = await createPaymentTerm(data); if (result.data) { - toast.success("결제 조건이 추가되었습니다."); + toast.success("결제 조건이 성공적으로 추가되었습니다."); + form.reset(); setOpen(false); if (onSuccess) { onSuccess(); @@ -89,16 +90,17 @@ export function PaymentTermsAddDialog({ onSuccess }: PaymentTermsAddDialogProps) return ( <Dialog open={open} onOpenChange={handleOpenChange}> <DialogTrigger asChild> - <Button size="sm" variant="outline"> + <Button variant="outline" size="sm"> <Plus className="mr-2 h-4 w-4" /> 결제 조건 추가 </Button> </DialogTrigger> <DialogContent className="max-w-md"> <DialogHeader> - <DialogTitle>결제 조건 추가</DialogTitle> + <DialogTitle>새 결제 조건 추가</DialogTitle> <DialogDescription> 새로운 결제 조건을 추가합니다. 필수 정보를 입력해주세요. + <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span> </DialogDescription> </DialogHeader> @@ -153,7 +155,7 @@ export function PaymentTermsAddDialog({ onSuccess }: PaymentTermsAddDialogProps) disabled={isLoading} > {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} - {isLoading ? "생성 중..." : "결제 조건 추가"} + {isLoading ? "생성 중..." : "추가"} </Button> </DialogFooter> </DialogContent> diff --git a/lib/payment-terms/table/payment-terms-edit-sheet.tsx b/lib/payment-terms/table/payment-terms-edit-sheet.tsx index b0d105bc..48d79c21 100644 --- a/lib/payment-terms/table/payment-terms-edit-sheet.tsx +++ b/lib/payment-terms/table/payment-terms-edit-sheet.tsx @@ -5,6 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import { toast } from "sonner" import * as z from "zod" +import { Loader } from "lucide-react" import { Button } from "@/components/ui/button" import { @@ -17,8 +18,10 @@ import { } from "@/components/ui/form" import { Sheet, + SheetClose, SheetContent, SheetDescription, + SheetFooter, SheetHeader, SheetTitle, } from "@/components/ui/sheet" @@ -38,7 +41,7 @@ type UpdatePaymentTermSchema = z.infer<typeof updatePaymentTermSchema> interface PaymentTermsEditSheetProps { open: boolean onOpenChange: (open: boolean) => void - data: typeof paymentTerms.$inferSelect + data: typeof paymentTerms.$inferSelect | null onSuccess: () => void } @@ -48,12 +51,14 @@ export function PaymentTermsEditSheet({ data, onSuccess, }: PaymentTermsEditSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + const form = useForm<UpdatePaymentTermSchema>({ resolver: zodResolver(updatePaymentTermSchema), defaultValues: { - code: data.code, - description: data.description, - isActive: data.isActive, + code: data?.code ?? "", + description: data?.description ?? "", + isActive: data?.isActive ?? true, }, mode: "onChange" }) @@ -69,14 +74,19 @@ export function PaymentTermsEditSheet({ }, [data, form]) async function onSubmit(input: UpdatePaymentTermSchema) { - try { - await updatePaymentTerm(data.code, input) - toast.success("수정이 완료되었습니다.") - onSuccess() - onOpenChange(false) - } catch { - toast.error("수정 중 오류가 발생했습니다.") - } + if (!data) return + + startUpdateTransition(async () => { + try { + await updatePaymentTerm(data.code, input) + toast.success("결제 조건이 성공적으로 수정되었습니다.") + onSuccess() + onOpenChange(false) + } catch (error) { + console.error("Update error:", error) + toast.error("결제 조건 수정 중 오류가 발생했습니다.") + } + }) } return ( @@ -97,7 +107,7 @@ export function PaymentTermsEditSheet({ <FormItem> <FormLabel>코드</FormLabel> <FormControl> - <Input {...field} disabled /> + <Input {...field} /> </FormControl> <FormMessage /> </FormItem> @@ -133,12 +143,25 @@ export function PaymentTermsEditSheet({ </FormItem> )} /> - <div className="flex justify-end space-x-2"> - <Button type="button" variant="outline" onClick={() => onOpenChange(false)}> - 취소 + <SheetFooter className="gap-2 pt-2 sm:space-x-0"> + <SheetClose asChild> + <Button type="button" variant="outline"> + 취소 + </Button> + </SheetClose> + <Button + type="submit" + disabled={isUpdatePending || !form.formState.isValid} + > + {isUpdatePending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + 저장 </Button> - <Button type="submit">저장</Button> - </div> + </SheetFooter> </form> </Form> </SheetContent> diff --git a/lib/payment-terms/table/payment-terms-table-columns.tsx b/lib/payment-terms/table/payment-terms-table-columns.tsx index 208723f7..08d30482 100644 --- a/lib/payment-terms/table/payment-terms-table-columns.tsx +++ b/lib/payment-terms/table/payment-terms-table-columns.tsx @@ -1,76 +1,71 @@ -import { type ColumnDef, type Row } from "@tanstack/react-table"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Ellipsis } from "lucide-react"; +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Ellipsis } from "lucide-react" + +import { formatDateTime } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, + DropdownMenuShortcut, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { paymentTerms } from "@/db/schema/procurementRFQ"; -import { toast } from "sonner"; -import { deletePaymentTerm } from "../service"; +} from "@/components/ui/dropdown-menu" -type PaymentTerm = typeof paymentTerms.$inferSelect; +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { paymentTerms } from "@/db/schema/procurementRFQ" interface GetColumnsProps { - setRowAction: (action: { type: string; row: Row<PaymentTerm> }) => void; - onSuccess: () => void; + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<typeof paymentTerms.$inferSelect> | null>> } -const handleDelete = async (code: string, onSuccess: () => void) => { - const result = await deletePaymentTerm(code); - if (result.success) { - toast.success("삭제 완료"); - onSuccess(); - } else { - toast.error(result.error || "삭제 실패"); +/** + * tanstack table 컬럼 정의 + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof paymentTerms.$inferSelect>[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef<typeof paymentTerms.$inferSelect> = { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + maxSize: 30, + enableSorting: false, + enableHiding: false, } -}; -export function getColumns({ setRowAction, onSuccess }: GetColumnsProps): ColumnDef<PaymentTerm>[] { - return [ - { - id: "code", - header: () => <div>코드</div>, - cell: ({ row }) => <div>{row.original.code}</div>, - enableSorting: true, - enableHiding: false, - }, - { - id: "description", - header: () => <div>설명</div>, - cell: ({ row }) => <div>{row.original.description}</div>, - enableSorting: true, - enableHiding: false, - }, - { - id: "isActive", - header: () => <div>상태</div>, - cell: ({ row }) => ( - <Badge variant={row.original.isActive ? "default" : "secondary"}> - {row.original.isActive ? "활성" : "비활성"} - </Badge> - ), - enableSorting: true, - enableHiding: false, - }, - { - id: "createdAt", - header: () => <div>생성일</div>, - cell: ({ row }) => { - const value = row.original.createdAt; - const date = value ? new Date(value) : null; - return date ? date.toLocaleDateString() : ""; - }, - enableSorting: true, - enableHiding: false, - }, - { - id: "actions", - cell: ({ row }) => ( + // ---------------------------------------------------------------- + // 2) actions 컬럼 (Dropdown 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef<typeof paymentTerms.$inferSelect> = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button @@ -83,20 +78,99 @@ export function getColumns({ setRowAction, onSuccess }: GetColumnsProps): Column </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-40"> <DropdownMenuItem - onSelect={() => setRowAction({ type: "edit", row })} + onSelect={() => setRowAction({ row, type: "update" })} > - 수정 + Edit </DropdownMenuItem> + <DropdownMenuSeparator /> <DropdownMenuItem - onSelect={() => handleDelete(row.original.code, onSuccess)} - className="text-destructive" + onSelect={() => setRowAction({ row, type: "delete" })} > - 삭제 + Delete + <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> + ) + }, + maxSize: 30, + } + + // ---------------------------------------------------------------- + // 3) 데이터 컬럼들 + // ---------------------------------------------------------------- + const dataColumns: ColumnDef<typeof paymentTerms.$inferSelect>[] = [ + { + accessorKey: "code", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="코드" /> + ), + meta: { + excelHeader: "코드", + type: "text", + }, + cell: ({ row }) => row.getValue("code") ?? "", + minSize: 80 + }, + { + accessorKey: "description", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="설명" /> ), + meta: { + excelHeader: "설명", + type: "text", + }, + cell: ({ row }) => row.getValue("description") ?? "", + minSize: 80 }, - ]; + { + accessorKey: "isActive", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="상태" /> + ), + meta: { + excelHeader: "상태", + type: "boolean", + }, + cell: ({ row }) => { + const isActive = row.getValue("isActive") as boolean + return ( + <Badge variant={isActive ? "default" : "secondary"}> + {isActive ? "활성" : "비활성"} + </Badge> + ) + }, + minSize: 80 + }, + { + accessorKey: "createdAt", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="생성일" /> + ), + meta: { + excelHeader: "생성일", + type: "date", + }, + cell: ({ row }) => { + const dateVal = row.getValue("createdAt") as Date + return formatDateTime(dateVal) + }, + minSize: 80 + } + ] + + // ---------------------------------------------------------------- + // 4) 최종 컬럼 배열: select, dataColumns, actions + // ---------------------------------------------------------------- + return [ + selectColumn, + ...dataColumns, + actionsColumn, + ] }
\ No newline at end of file diff --git a/lib/payment-terms/table/payment-terms-table-toolbar.tsx b/lib/payment-terms/table/payment-terms-table-toolbar.tsx index 2466a9e4..51ac9b93 100644 --- a/lib/payment-terms/table/payment-terms-table-toolbar.tsx +++ b/lib/payment-terms/table/payment-terms-table-toolbar.tsx @@ -1,16 +1,53 @@ "use client"; import * as React from "react"; +import { type Table } from "@tanstack/react-table"; +import { Download } from "lucide-react"; + +import { exportTableToExcel } from "@/lib/export"; +import { Button } from "@/components/ui/button"; +import { DeletePaymentTermsDialog } from "./delete-payment-terms-dialog"; import { PaymentTermsAddDialog } from "./payment-terms-add-dialog"; +import { paymentTerms } from "@/db/schema/procurementRFQ"; -interface PaymentTermsTableToolbarProps { +interface PaymentTermsTableToolbarActionsProps { + table: Table<typeof paymentTerms.$inferSelect>; onSuccess?: () => void; } -export function PaymentTermsTableToolbar({ onSuccess }: PaymentTermsTableToolbarProps) { +export function PaymentTermsTableToolbarActions({ table, onSuccess }: PaymentTermsTableToolbarActionsProps) { return ( <div className="flex items-center gap-2"> + {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */} + {table.getFilteredSelectedRowModel().rows.length > 0 ? ( + <DeletePaymentTermsDialog + paymentTerms={table + .getFilteredSelectedRowModel() + .rows.map((row) => row.original)} + onSuccess={() => { + table.toggleAllRowsSelected(false); + onSuccess?.(); + }} + /> + ) : null} + <PaymentTermsAddDialog onSuccess={onSuccess} /> + + {/** 3) Export 버튼 */} + <Button + variant="outline" + size="sm" + onClick={() => + exportTableToExcel(table, { + filename: "payment-terms-list", + excludeColumns: ["select", "actions"], + }) + } + className="gap-2" + > + <Download className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">Export</span> + </Button> </div> ); }
\ No newline at end of file diff --git a/lib/payment-terms/table/payment-terms-table.tsx b/lib/payment-terms/table/payment-terms-table.tsx index 589acb52..ddf270ce 100644 --- a/lib/payment-terms/table/payment-terms-table.tsx +++ b/lib/payment-terms/table/payment-terms-table.tsx @@ -3,13 +3,16 @@ import * as React from "react"; import { useDataTable } from "@/hooks/use-data-table"; import { DataTable } from "@/components/data-table/data-table"; import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"; +import type { + DataTableAdvancedFilterField, + DataTableRowAction, +} from "@/types/table" +import { getPaymentTerms } from "../service"; import { getColumns } from "./payment-terms-table-columns"; -import { paymentTerms } from "@/db/schema/procurementRFQ"; -import { PaymentTermsTableToolbar } from "./payment-terms-table-toolbar"; -import { toast } from "sonner"; +import { DeletePaymentTermsDialog } from "./delete-payment-terms-dialog"; import { PaymentTermsEditSheet } from "./payment-terms-edit-sheet"; -import { Row } from "@tanstack/react-table"; -import { getPaymentTerms } from "../service"; +import { PaymentTermsTableToolbarActions } from "./payment-terms-table-toolbar"; +import { paymentTerms } from "@/db/schema/procurementRFQ"; import { GetPaymentTermsSchema } from "../validations"; interface PaymentTermsTableProps { @@ -18,8 +21,7 @@ interface PaymentTermsTableProps { export function PaymentTermsTable({ promises }: PaymentTermsTableProps) { const [rawData, setRawData] = React.useState<{ data: typeof paymentTerms.$inferSelect[]; pageCount: number }>({ data: [], pageCount: 0 }); - const [isEditDialogOpen, setIsEditDialogOpen] = React.useState(false); - const [selectedRow, setSelectedRow] = React.useState<typeof paymentTerms.$inferSelect | null>(null); + const [rowAction, setRowAction] = React.useState<DataTableRowAction<typeof paymentTerms.$inferSelect> | null>(null); React.useEffect(() => { if (promises) { @@ -45,7 +47,6 @@ export function PaymentTermsTable({ promises }: PaymentTermsTableProps) { setRawData(result); } catch (error) { console.error("Error refreshing data:", error); - toast.error("데이터를 불러오는 중 오류가 발생했습니다."); } })(); } @@ -78,50 +79,71 @@ export function PaymentTermsTable({ promises }: PaymentTermsTableProps) { setRawData(result); } catch (error) { console.error("Error refreshing data:", error); - toast.error("데이터를 불러오는 중 오류가 발생했습니다."); } }, [fetchPaymentTerms]); - const handleRowAction = async (action: { type: string; row: Row<typeof paymentTerms.$inferSelect> }) => { - if (action.type === "edit") { - setSelectedRow(action.row.original); - setIsEditDialogOpen(true); - } - }; + // 컬럼 설정 - 외부 파일에서 가져옴 + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) - const columns = React.useMemo(() => getColumns({ setRowAction: handleRowAction, onSuccess: refreshData }), [refreshData]); + // 고급 필터 필드 설정 + const advancedFilterFields: DataTableAdvancedFilterField<typeof paymentTerms.$inferSelect>[] = [ + { id: "code", label: "코드", type: "text" }, + { + id: "isActive", label: "상태", type: "select", options: [ + { label: "활성", value: "true" }, + { label: "비활성", value: "false" }, + ] + }, + { id: "description", label: "설명", type: "text" }, + { id: "createdAt", label: "생성일", type: "date" }, + ]; const { table } = useDataTable({ - data: rawData.data, - columns, - pageCount: rawData.pageCount, - filterFields: [], - enablePinning: true, - enableAdvancedFilter: true, - initialState: { - sorting: [{ id: "createdAt", desc: true }], - columnPinning: { right: ["actions"] }, - }, - getRowId: (originalRow) => String(originalRow.code), - shallow: false, - clearOnDefault: true, - }); + data: rawData.data, + columns, + pageCount: rawData.pageCount, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.code), + shallow: false, + clearOnDefault: true, + }) return ( <> <DataTable table={table}> - <DataTableAdvancedToolbar table={table} filterFields={[]} shallow={false}> - <PaymentTermsTableToolbar onSuccess={refreshData} /> + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + > + <PaymentTermsTableToolbarActions table={table} onSuccess={refreshData} /> </DataTableAdvancedToolbar> </DataTable> - {isEditDialogOpen && selectedRow && ( - <PaymentTermsEditSheet - open={isEditDialogOpen} - onOpenChange={setIsEditDialogOpen} - data={selectedRow} - onSuccess={refreshData} - /> - )} + + <DeletePaymentTermsDialog + open={rowAction?.type === "delete"} + onOpenChange={() => setRowAction(null)} + paymentTerms={rowAction?.row.original ? [rowAction?.row.original] : []} + showTrigger={false} + onSuccess={() => { + rowAction?.row.toggleSelected(false) + refreshData() + }} + /> + + <PaymentTermsEditSheet + open={rowAction?.type === "update"} + onOpenChange={() => setRowAction(null)} + data={rowAction?.row.original ?? null} + onSuccess={refreshData} + /> </> ); }
\ No newline at end of file diff --git a/lib/project-gtc/service.ts b/lib/project-gtc/service.ts new file mode 100644 index 00000000..c65d9364 --- /dev/null +++ b/lib/project-gtc/service.ts @@ -0,0 +1,389 @@ +"use server"; + +import { revalidateTag } from "next/cache"; +import db from "@/db/db"; +import { unstable_cache } from "@/lib/unstable-cache"; +import { asc, desc, ilike, or, eq, count, and, ne, sql } from "drizzle-orm"; +import { + projectGtcFiles, + projectGtcView, + type ProjectGtcFile, + projects, +} from "@/db/schema"; +import { promises as fs } from "fs"; +import path from "path"; +import crypto from "crypto"; +import { revalidatePath } from 'next/cache'; + +// Project GTC 목록 조회 +export async function getProjectGtcList( + input: { + page: number; + perPage: number; + search?: string; + sort: Array<{ id: string; desc: boolean }>; + filters?: Record<string, unknown>; + } +) { + return unstable_cache( + async () => { + try { + const offset = (input.page - 1) * input.perPage; + + const { data, total } = await db.transaction(async (tx) => { + let whereCondition = undefined; + + // GTC 파일이 있는 프로젝트만 필터링 + const gtcFileCondition = sql`${projectGtcView.gtcFileId} IS NOT NULL`; + + if (input.search) { + const s = `%${input.search}%`; + const searchCondition = or( + ilike(projectGtcView.code, s), + ilike(projectGtcView.name, s), + ilike(projectGtcView.type, s), + ilike(projectGtcView.originalFileName, s) + ); + whereCondition = and(gtcFileCondition, searchCondition); + } else { + whereCondition = gtcFileCondition; + } + + const orderBy = + input.sort.length > 0 + ? input.sort.map((item) => + item.desc + ? desc( + projectGtcView[ + item.id as keyof typeof projectGtcView + ] as never + ) + : asc( + projectGtcView[ + item.id as keyof typeof projectGtcView + ] as never + ) + ) + : [desc(projectGtcView.projectCreatedAt)]; + + const dataResult = await tx + .select() + .from(projectGtcView) + .where(whereCondition) + .orderBy(...orderBy) + .limit(input.perPage) + .offset(offset); + + const totalCount = await tx + .select({ count: count() }) + .from(projectGtcView) + .where(whereCondition); + + return { + data: dataResult, + total: totalCount[0]?.count || 0, + }; + }); + + const pageCount = Math.ceil(total / input.perPage); + + return { + data, + pageCount, + }; + } catch (error) { + console.error("getProjectGtcList 에러:", error); + throw new Error("Project GTC 목록을 가져오는 중 오류가 발생했습니다."); + } + }, + [`project-gtc-list-${JSON.stringify(input)}`], + { + tags: ["project-gtc"], + revalidate: false, + } + )(); +} + +// Project GTC 파일 업로드 +export async function uploadProjectGtcFile( + projectId: number, + file: File +): Promise<{ success: boolean; data?: ProjectGtcFile; error?: string }> { + try { + // 유효성 검사 + if (!projectId) { + return { success: false, error: "프로젝트 ID는 필수입니다." }; + } + + if (!file) { + return { success: false, error: "파일은 필수입니다." }; + } + + // 허용된 파일 타입 검사 + const allowedTypes = [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'text/plain' + ]; + + if (!allowedTypes.includes(file.type)) { + return { success: false, error: "PDF, Word, 또는 텍스트 파일만 업로드 가능합니다." }; + } + + // 원본 파일 이름과 확장자 분리 + const originalFileName = file.name; + const fileExtension = path.extname(originalFileName); + const fileNameWithoutExt = path.basename(originalFileName, fileExtension); + + // 해시된 파일 이름 생성 + const timestamp = Date.now(); + const randomHash = crypto.createHash('md5') + .update(`${fileNameWithoutExt}-${timestamp}-${Math.random()}`) + .digest('hex') + .substring(0, 8); + + const hashedFileName = `${timestamp}-${randomHash}${fileExtension}`; + + // 저장 디렉토리 설정 + const uploadDir = path.join(process.cwd(), "public", "project-gtc"); + + // 디렉토리가 없으면 생성 + try { + await fs.mkdir(uploadDir, { recursive: true }); + } catch (err) { + console.log("Directory already exists or creation failed:", err); + } + + // 파일 경로 설정 + const filePath = path.join(uploadDir, hashedFileName); + const publicFilePath = `/project-gtc/${hashedFileName}`; + + // 파일을 ArrayBuffer로 변환 + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // 파일 저장 + await fs.writeFile(filePath, buffer); + + // 기존 파일이 있으면 삭제 + const existingFile = await db.query.projectGtcFiles.findFirst({ + where: eq(projectGtcFiles.projectId, projectId) + }); + + if (existingFile) { + // 기존 파일 삭제 + try { + const filePath = path.join(process.cwd(), "public", existingFile.filePath); + await fs.unlink(filePath); + } catch { + console.error("파일 삭제 실패"); + } + + // DB에서 기존 파일 정보 삭제 + await db.delete(projectGtcFiles) + .where(eq(projectGtcFiles.id, existingFile.id)); + } + + // DB에 새 파일 정보 저장 + const newFile = await db.insert(projectGtcFiles).values({ + projectId, + fileName: hashedFileName, + filePath: publicFilePath, + originalFileName, + fileSize: file.size, + mimeType: file.type, + }).returning(); + + revalidateTag("project-gtc"); + revalidatePath("/evcp/project-gtc"); + + return { success: true, data: newFile[0] }; + + } catch (error) { + console.error("Project GTC 파일 업로드 에러:", error); + return { + success: false, + error: error instanceof Error ? error.message : "파일 업로드 중 오류가 발생했습니다." + }; + } +} + +// Project GTC 파일 삭제 +export async function deleteProjectGtcFile( + projectId: number +): Promise<{ success: boolean; error?: string }> { + try { + return await db.transaction(async (tx) => { + const existingFile = await tx.query.projectGtcFiles.findFirst({ + where: eq(projectGtcFiles.projectId, projectId) + }); + + if (!existingFile) { + return { success: false, error: "삭제할 파일이 없습니다." }; + } + + // 파일 시스템에서 파일 삭제 + try { + const filePath = path.join(process.cwd(), "public", existingFile.filePath); + await fs.unlink(filePath); + } catch (error) { + console.error("파일 시스템에서 파일 삭제 실패:", error); + throw new Error("파일 시스템에서 파일 삭제에 실패했습니다."); + } + + // DB에서 파일 정보 삭제 + await tx.delete(projectGtcFiles) + .where(eq(projectGtcFiles.id, existingFile.id)); + + return { success: true }; + }); + + } catch (error) { + console.error("Project GTC 파일 삭제 에러:", error); + return { + success: false, + error: error instanceof Error ? error.message : "파일 삭제 중 오류가 발생했습니다." + }; + } finally { + // 트랜잭션 성공/실패와 관계없이 캐시 무효화 + revalidateTag("project-gtc"); + revalidatePath("/evcp/project-gtc"); + } +} + +// 프로젝트별 GTC 파일 정보 조회 +export async function getProjectGtcFile(projectId: number): Promise<ProjectGtcFile | null> { + try { + const file = await db.query.projectGtcFiles.findFirst({ + where: eq(projectGtcFiles.projectId, projectId) + }); + + return file || null; + } catch (error) { + console.error("Project GTC 파일 조회 에러:", error); + return null; + } +} + +// 프로젝트 생성 서버 액션 +export async function createProject( + input: { + code: string; + name: string; + type: string; + } +): Promise<{ success: boolean; data?: typeof projects.$inferSelect; error?: string }> { + try { + // 유효성 검사 + if (!input.code?.trim()) { + return { success: false, error: "프로젝트 코드는 필수입니다." }; + } + + if (!input.name?.trim()) { + return { success: false, error: "프로젝트명은 필수입니다." }; + } + + if (!input.type?.trim()) { + return { success: false, error: "프로젝트 타입은 필수입니다." }; + } + + // 프로젝트 코드 중복 검사 + const existingProject = await db.query.projects.findFirst({ + where: eq(projects.code, input.code.trim()) + }); + + if (existingProject) { + return { success: false, error: "이미 존재하는 프로젝트 코드입니다." }; + } + + // 프로젝트 생성 + const newProject = await db.insert(projects).values({ + code: input.code.trim(), + name: input.name.trim(), + type: input.type.trim(), + }).returning(); + + revalidateTag("project-gtc"); + revalidatePath("/evcp/project-gtc"); + + return { success: true, data: newProject[0] }; + + } catch (error) { + console.error("프로젝트 생성 에러:", error); + return { + success: false, + error: error instanceof Error ? error.message : "프로젝트 생성 중 오류가 발생했습니다." + }; + } +} + +// 프로젝트 정보 수정 서버 액션 +export async function updateProject( + input: { + id: number; + code: string; + name: string; + type: string; + } +): Promise<{ success: boolean; error?: string }> { + try { + if (!input.id) { + return { success: false, error: "프로젝트 ID는 필수입니다." }; + } + if (!input.code?.trim()) { + return { success: false, error: "프로젝트 코드는 필수입니다." }; + } + if (!input.name?.trim()) { + return { success: false, error: "프로젝트명은 필수입니다." }; + } + if (!input.type?.trim()) { + return { success: false, error: "프로젝트 타입은 필수입니다." }; + } + + // 프로젝트 코드 중복 검사 (본인 제외) + const existingProject = await db.query.projects.findFirst({ + where: and( + eq(projects.code, input.code.trim()), + ne(projects.id, input.id) + ) + }); + if (existingProject) { + return { success: false, error: "이미 존재하는 프로젝트 코드입니다." }; + } + + // 업데이트 + await db.update(projects) + .set({ + code: input.code.trim(), + name: input.name.trim(), + type: input.type.trim(), + }) + .where(eq(projects.id, input.id)); + + revalidateTag("project-gtc"); + revalidatePath("/evcp/project-gtc"); + + return { success: true }; + } catch (error) { + console.error("프로젝트 수정 에러:", error); + return { + success: false, + error: error instanceof Error ? error.message : "프로젝트 수정 중 오류가 발생했습니다." + }; + } +} + +// 이미 GTC 파일이 등록된 프로젝트 ID 목록 조회 +export async function getProjectsWithGtcFiles(): Promise<number[]> { + try { + const result = await db + .select({ projectId: projectGtcFiles.projectId }) + .from(projectGtcFiles); + + return result.map(row => row.projectId); + } catch (error) { + console.error("GTC 파일이 등록된 프로젝트 조회 에러:", error); + return []; + } +}
\ No newline at end of file diff --git a/lib/project-gtc/table/add-project-dialog.tsx b/lib/project-gtc/table/add-project-dialog.tsx new file mode 100644 index 00000000..616ab950 --- /dev/null +++ b/lib/project-gtc/table/add-project-dialog.tsx @@ -0,0 +1,296 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { toast } from "sonner" +import { Upload, X } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { ProjectSelector } from "@/components/ProjectSelector" +import { uploadProjectGtcFile, getProjectsWithGtcFiles } from "../service" +import { type Project } from "@/lib/rfqs/service" + +const addProjectSchema = z.object({ + projectId: z.number().min(1, "프로젝트 선택은 필수입니다."), + gtcFile: z.instanceof(File, { message: "GTC 파일은 필수입니다." }).optional(), +}) + +type AddProjectFormValues = z.infer<typeof addProjectSchema> + +interface AddProjectDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onSuccess?: () => void +} + +export function AddProjectDialog({ + open, + onOpenChange, + onSuccess, +}: AddProjectDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [selectedProject, setSelectedProject] = React.useState<Project | null>(null) + const [selectedFile, setSelectedFile] = React.useState<File | null>(null) + const [excludedProjectIds, setExcludedProjectIds] = React.useState<number[]>([]) + + const form = useForm<AddProjectFormValues>({ + resolver: zodResolver(addProjectSchema), + defaultValues: { + projectId: 0, + gtcFile: undefined, + }, + }) + + // 이미 GTC 파일이 등록된 프로젝트 ID 목록 로드 + React.useEffect(() => { + async function loadExcludedProjects() { + try { + const excludedIds = await getProjectsWithGtcFiles(); + setExcludedProjectIds(excludedIds); + } catch (error) { + console.error("제외할 프로젝트 목록 로드 오류:", error); + } + } + + if (open) { + loadExcludedProjects(); + } + }, [open]); + + // 프로젝트 선택 시 폼에 자동으로 채우기 + const handleProjectSelect = (project: Project) => { + // 이미 GTC 파일이 등록된 프로젝트인지 확인 + if (excludedProjectIds.includes(project.id)) { + toast.error("이미 GTC 파일이 등록된 프로젝트입니다."); + // 선택된 프로젝트 정보 초기화 + setSelectedProject(null); + form.setValue("projectId", 0); + return; + } + + setSelectedProject(project) + form.setValue("projectId", project.id) + } + + // 파일 선택 처리 + const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => { + const file = event.target.files?.[0] + if (file) { + // PDF 파일만 허용 + if (file.type !== 'application/pdf') { + toast.error("PDF 파일만 업로드 가능합니다.") + return + } + + setSelectedFile(file) + form.setValue("gtcFile", file) + } + } + + // 파일 제거 + const handleRemoveFile = () => { + setSelectedFile(null) + form.setValue("gtcFile", undefined) + // input 요소의 value도 초기화 + const fileInput = document.getElementById('gtc-file-input') as HTMLInputElement + if (fileInput) { + fileInput.value = '' + } + } + + const onSubmit = async (data: AddProjectFormValues) => { + // 프로젝트가 선택되지 않았으면 에러 + if (!selectedProject) { + toast.error("프로젝트를 선택해주세요.") + return + } + + // 이미 GTC 파일이 등록된 프로젝트인지 다시 한번 확인 + if (excludedProjectIds.includes(selectedProject.id)) { + toast.error("이미 GTC 파일이 등록된 프로젝트입니다.") + return + } + + // GTC 파일이 없으면 에러 + if (!data.gtcFile) { + toast.error("GTC 파일은 필수입니다.") + return + } + + setIsLoading(true) + try { + // GTC 파일 업로드 + const fileResult = await uploadProjectGtcFile(selectedProject.id, data.gtcFile) + + if (!fileResult.success) { + toast.error(fileResult.error || "GTC 파일 업로드에 실패했습니다.") + return + } + + toast.success("GTC 파일이 성공적으로 업로드되었습니다.") + form.reset() + setSelectedProject(null) + setSelectedFile(null) + onOpenChange(false) + onSuccess?.() + } catch (error) { + console.error("GTC 파일 업로드 오류:", error) + toast.error("GTC 파일 업로드 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + const handleOpenChange = (newOpen: boolean) => { + if (!newOpen) { + form.reset() + setSelectedProject(null) + setSelectedFile(null) + } + onOpenChange(newOpen) + } + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + <DialogContent className="sm:max-w-[500px]"> + <DialogHeader> + <DialogTitle>GTC 파일 추가</DialogTitle> + <DialogDescription> + 기존 프로젝트를 선택하고 GTC 파일을 업로드합니다. (이미 GTC 파일이 등록된 프로젝트는 제외됩니다) + </DialogDescription> + </DialogHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + {/* 프로젝트 선택 (필수) */} + <FormField + control={form.control} + name="projectId" + render={() => ( + <FormItem> + <FormLabel>프로젝트 선택 *</FormLabel> + <FormControl> + <ProjectSelector + selectedProjectId={selectedProject?.id} + onProjectSelect={handleProjectSelect} + placeholder="프로젝트를 선택하세요..." + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 선택된 프로젝트 정보 표시 (읽기 전용) */} + {selectedProject && ( + <div className="p-4 bg-muted rounded-lg space-y-2"> + <h4 className="font-medium text-sm">선택된 프로젝트 정보</h4> + <div className="space-y-1 text-sm"> + <div className="flex justify-between"> + <span className="text-muted-foreground">프로젝트 코드:</span> + <span className="font-medium">{selectedProject.projectCode}</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">프로젝트명:</span> + <span className="font-medium">{selectedProject.projectName}</span> + </div> + </div> + </div> + )} + + {/* GTC 파일 업로드 */} + <FormField + control={form.control} + name="gtcFile" + render={() => ( + <FormItem> + <FormLabel>GTC 파일 *</FormLabel> + <div className="space-y-2"> + {!selectedFile ? ( + <div className="flex items-center justify-center w-full"> + <label + htmlFor="gtc-file-input" + className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100" + > + <div className="flex flex-col items-center justify-center pt-5 pb-6"> + <Upload className="w-8 h-8 mb-4 text-gray-500" /> + <p className="mb-2 text-sm text-gray-500"> + <span className="font-semibold">클릭하여 파일 선택</span> 또는 드래그 앤 드롭 + </p> + <p className="text-xs text-gray-500"> + PDF 파일만 + </p> + </div> + <input + id="gtc-file-input" + type="file" + className="hidden" + accept=".pdf" + onChange={handleFileSelect} + disabled={isLoading} + /> + </label> + </div> + ) : ( + <div className="flex items-center justify-between p-3 border rounded-lg bg-gray-50"> + <div className="flex items-center space-x-2"> + <Upload className="w-4 h-4 text-gray-500" /> + <span className="text-sm font-medium">{selectedFile.name}</span> + <span className="text-xs text-gray-500"> + ({(selectedFile.size / 1024 / 1024).toFixed(2)} MB) + </span> + </div> + <Button + type="button" + variant="ghost" + size="sm" + onClick={handleRemoveFile} + disabled={isLoading} + > + <X className="w-4 h-4" /> + </Button> + </div> + )} + </div> + <FormMessage /> + </FormItem> + )} + /> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button type="submit" disabled={isLoading || !selectedProject}> + {isLoading ? "업로드 중..." : "GTC 파일 업로드"} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file diff --git a/lib/project-gtc/table/delete-gtc-file-dialog.tsx b/lib/project-gtc/table/delete-gtc-file-dialog.tsx new file mode 100644 index 00000000..d64be529 --- /dev/null +++ b/lib/project-gtc/table/delete-gtc-file-dialog.tsx @@ -0,0 +1,160 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, Trash } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" +import { deleteProjectGtcFile } from "../service" +import { ProjectGtcView } from "@/db/schema" + +interface DeleteGtcFileDialogProps + extends React.ComponentPropsWithoutRef<typeof Dialog> { + projects: Row<ProjectGtcView>["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeleteGtcFileDialog({ + projects, + showTrigger = true, + onSuccess, + ...props +}: DeleteGtcFileDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onDelete() { + startDeleteTransition(async () => { + try { + // 각 프로젝트의 GTC 파일을 삭제 + const deletePromises = projects.map(project => + deleteProjectGtcFile(project.id) + ) + + const results = await Promise.all(deletePromises) + + // 성공/실패 확인 + const successCount = results.filter(result => result.success).length + const failureCount = results.length - successCount + + if (failureCount > 0) { + toast.error(`${failureCount}개 파일 삭제에 실패했습니다.`) + return + } + + props.onOpenChange?.(false) + toast.success(`${successCount}개 GTC 파일이 성공적으로 삭제되었습니다.`) + onSuccess?.() + } catch (error) { + console.error("Delete error:", error) + toast.error("파일 삭제 중 오류가 발생했습니다.") + } + }) + } + + if (isDesktop) { + return ( + <Dialog {...props}> + {showTrigger ? ( + <DialogTrigger asChild> + <Button variant="outline" size="sm"> + <Trash className="mr-2 size-4" aria-hidden="true" /> + Delete ({projects.length}) + </Button> + </DialogTrigger> + ) : null} + <DialogContent> + <DialogHeader> + <DialogTitle>Are you absolutely sure?</DialogTitle> + <DialogDescription> + This action cannot be undone. This will permanently delete{" "} + <span className="font-medium">{projects.length}</span> + {projects.length === 1 ? " Project GTC" : " Project GTCs"} from our servers. + </DialogDescription> + </DialogHeader> + <DialogFooter className="gap-2 sm:space-x-0"> + <DialogClose asChild> + <Button variant="outline">Cancel</Button> + </DialogClose> + <Button + aria-label="Delete selected rows" + variant="destructive" + onClick={onDelete} + disabled={isDeletePending} + > + {isDeletePending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + Delete + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) + } + + return ( + <Drawer {...props}> + {showTrigger ? ( + <DrawerTrigger asChild> + <Button variant="outline" size="sm"> + <Trash className="mr-2 size-4" aria-hidden="true" /> + Delete ({projects.length}) + </Button> + </DrawerTrigger> + ) : null} + <DrawerContent> + <DrawerHeader> + <DrawerTitle>Are you absolutely sure?</DrawerTitle> + <DrawerDescription> + This action cannot be undone. This will permanently delete{" "} + <span className="font-medium">{projects.length}</span> + {projects.length === 1 ? " Project GTC" : " Project GTCs"} from our servers. + </DrawerDescription> + </DrawerHeader> + <DrawerFooter className="gap-2 sm:space-x-0"> + <DrawerClose asChild> + <Button variant="outline">Cancel</Button> + </DrawerClose> + <Button + aria-label="Delete selected rows" + variant="destructive" + onClick={onDelete} + disabled={isDeletePending} + > + {isDeletePending && ( + <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> + )} + Delete + </Button> + </DrawerFooter> + </DrawerContent> + </Drawer> + ) +}
\ No newline at end of file diff --git a/lib/project-gtc/table/project-gtc-table-columns.tsx b/lib/project-gtc/table/project-gtc-table-columns.tsx new file mode 100644 index 00000000..dfdf1921 --- /dev/null +++ b/lib/project-gtc/table/project-gtc-table-columns.tsx @@ -0,0 +1,364 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Ellipsis, Paperclip, FileText } from "lucide-react" +import { toast } from "sonner" + +import { formatDate, formatDateTime } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { ProjectGtcView } from "@/db/schema" + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<ProjectGtcView> | null>> +} + +/** + * 파일 다운로드 함수 + */ +const handleFileDownload = async (projectId: number, fileName: string) => { + try { + // API를 통해 파일 다운로드 + const response = await fetch(`/api/project-gtc?action=download&projectId=${projectId}`, { + method: 'GET', + }); + + if (!response.ok) { + throw new Error('파일 다운로드에 실패했습니다.'); + } + + // 파일 blob 생성 + const blob = await response.blob(); + + // 다운로드 링크 생성 + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // 메모리 정리 + window.URL.revokeObjectURL(url); + + toast.success("파일 다운로드를 시작합니다."); + } catch (error) { + console.error("파일 다운로드 오류:", error); + toast.error("파일 다운로드 중 오류가 발생했습니다."); + } +}; + +/** + * tanstack table 컬럼 정의 (중첩 헤더 버전) + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ProjectGtcView>[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef<ProjectGtcView> = { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + maxSize: 30, + enableSorting: false, + enableHiding: false, + } + + // ---------------------------------------------------------------- + // 2) 파일 다운로드 컬럼 (아이콘) + // ---------------------------------------------------------------- + const downloadColumn: ColumnDef<ProjectGtcView> = { + id: "download", + header: "", + cell: ({ row }) => { + const project = row.original; + + if (!project.filePath || !project.originalFileName) { + return null; + } + + return ( + <Button + variant="ghost" + size="icon" + onClick={() => handleFileDownload(project.id, project.originalFileName!)} + title={`${project.originalFileName} 다운로드`} + className="hover:bg-muted" + > + <Paperclip className="h-4 w-4" /> + <span className="sr-only">다운로드</span> + </Button> + ); + }, + maxSize: 30, + enableSorting: false, + } + + // ---------------------------------------------------------------- + // 3) actions 컬럼 (Dropdown 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef<ProjectGtcView> = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + aria-label="Open menu" + variant="ghost" + className="flex size-8 p-0 data-[state=open]:bg-muted" + > + <Ellipsis className="size-4" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-40"> + <DropdownMenuItem + onSelect={() => setRowAction({ type: "upload", row })} + > + Edit + </DropdownMenuItem> + + <DropdownMenuSeparator /> + <DropdownMenuItem + onSelect={() => setRowAction({ type: "delete", row })} + > + Delete + <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) + }, + maxSize: 30, + } + + // ---------------------------------------------------------------- + // 4) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 + // ---------------------------------------------------------------- + // 4-1) groupMap: { [groupName]: ColumnDef<ProjectGtcView>[] } + const groupMap: Record<string, ColumnDef<ProjectGtcView>[]> = {} + + // 프로젝트 정보 그룹 + groupMap["기본 정보"] = [ + { + accessorKey: "code", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" /> + ), + cell: ({ row }) => { + return ( + <div className="flex space-x-2"> + <span className="max-w-[500px] truncate font-medium"> + {row.getValue("code")} + </span> + </div> + ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + minSize: 120, + }, + { + accessorKey: "name", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트명" /> + ), + cell: ({ row }) => { + return ( + <div className="flex space-x-2"> + <span className="max-w-[500px] truncate"> + {row.getValue("name")} + </span> + </div> + ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + minSize: 200, + }, + { + accessorKey: "type", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트 타입" /> + ), + cell: ({ row }) => { + const type = row.getValue("type") as string + return ( + <div className="flex w-[100px] items-center"> + <Badge variant="secondary"> + {type} + </Badge> + </div> + ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + minSize: 100, + }, + ] + + // 파일 정보 그룹 + groupMap["파일 정보"] = [ + { + accessorKey: "originalFileName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="GTC 파일" /> + ), + cell: ({ row }) => { + const fileName = row.getValue("originalFileName") as string | null + const filePath = row.original.filePath + + if (!fileName) { + return ( + <div className="flex items-center text-muted-foreground"> + <FileText className="mr-2 h-4 w-4" /> + <span>파일 없음</span> + </div> + ) + } + + return ( + <div className="flex items-center space-x-2"> + <FileText className="h-4 w-4" /> + <div className="flex flex-col"> + {filePath ? ( + <button + onClick={async (e) => { + e.preventDefault(); + e.stopPropagation(); + try { + // API를 통해 파일 다운로드 + const response = await fetch(`/api/project-gtc?action=download&projectId=${row.original.id}`, { + method: 'GET', + }); + + if (!response.ok) { + throw new Error('파일을 열 수 없습니다.'); + } + + // 파일 blob 생성 + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + window.open(url, '_blank'); + } catch (error) { + console.error("파일 미리보기 오류:", error); + toast.error("파일을 열 수 없습니다."); + } + }} + className="font-medium text-left hover:underline cursor-pointer text-blue-600 hover:text-blue-800 transition-colors" + title="클릭하여 파일 열기" + > + {fileName} + </button> + ) : ( + <span className="font-medium">{fileName}</span> + )} + </div> + </div> + ) + }, + minSize: 200, + }, + ] + + // 날짜 정보 그룹 + groupMap["날짜 정보"] = [ + { + accessorKey: "gtcCreatedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="GTC 등록일" /> + ), + cell: ({ row }) => { + const date = row.getValue("gtcCreatedAt") as Date | null + if (!date) { + return <span className="text-muted-foreground">-</span> + } + return ( + <div className="flex items-center"> + <span> + {formatDateTime(new Date(date))} + </span> + </div> + ) + }, + minSize: 150, + }, + { + accessorKey: "projectCreatedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트 생성일" /> + ), + cell: ({ row }) => { + const date = row.getValue("projectCreatedAt") as Date + return ( + <div className="flex items-center"> + <span> + {formatDate(new Date(date))} + </span> + </div> + ) + }, + minSize: 120, + }, + ] + + // ---------------------------------------------------------------- + // 4-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기 + // ---------------------------------------------------------------- + const nestedColumns: ColumnDef<ProjectGtcView>[] = [] + + // 순서를 고정하고 싶다면 group 순서를 미리 정의하거나 sort해야 함 + Object.entries(groupMap).forEach(([groupName, colDefs]) => { + // 상위 컬럼 + nestedColumns.push({ + id: groupName, + header: groupName, // "프로젝트 정보", "파일 정보", "날짜 정보" 등 + columns: colDefs, + }) + }) + + // ---------------------------------------------------------------- + // 5) 최종 컬럼 배열: select, download, nestedColumns, actions + // ---------------------------------------------------------------- + return [ + selectColumn, + downloadColumn, // 다운로드 컬럼 추가 + ...nestedColumns, + actionsColumn, + ] +}
\ No newline at end of file diff --git a/lib/project-gtc/table/project-gtc-table-toolbar-actions.tsx b/lib/project-gtc/table/project-gtc-table-toolbar-actions.tsx new file mode 100644 index 00000000..ec6ba053 --- /dev/null +++ b/lib/project-gtc/table/project-gtc-table-toolbar-actions.tsx @@ -0,0 +1,74 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { type Table } from "@tanstack/react-table" +import { Download, Plus } from "lucide-react" + +import { exportTableToExcel } from "@/lib/export" +import { Button } from "@/components/ui/button" +import { DeleteGtcFileDialog } from "./delete-gtc-file-dialog" +import { AddProjectDialog } from "./add-project-dialog" +import { ProjectGtcView } from "@/db/schema" + +interface ProjectGtcTableToolbarActionsProps { + table: Table<ProjectGtcView> +} + +export function ProjectGtcTableToolbarActions({ table }: ProjectGtcTableToolbarActionsProps) { + const router = useRouter() + const [showAddProjectDialog, setShowAddProjectDialog] = React.useState(false) + + return ( + <div className="flex items-center gap-2"> + {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */} + {table.getFilteredSelectedRowModel().rows.length > 0 ? ( + <DeleteGtcFileDialog + projects={table + .getFilteredSelectedRowModel() + .rows.map((row) => row.original)} + onSuccess={() => { + table.toggleAllRowsSelected(false) + router.refresh() + }} + /> + ) : null} + + {/** 2) GTC 추가 버튼 */} + <Button + variant="outline" + size="sm" + onClick={() => setShowAddProjectDialog(true)} + className="gap-2" + > + <Plus className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">GTC 추가</span> + </Button> + + {/** 3) Export 버튼 */} + <Button + variant="outline" + size="sm" + onClick={() => + exportTableToExcel(table, { + filename: "project-gtc-list", + excludeColumns: ["select", "download", "actions"], + }) + } + className="gap-2" + > + <Download className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">Export</span> + </Button> + + {/** 4) 프로젝트 추가 다이얼로그 */} + <AddProjectDialog + open={showAddProjectDialog} + onOpenChange={setShowAddProjectDialog} + onSuccess={() => { + router.refresh() + }} + /> + </div> + ) +}
\ No newline at end of file diff --git a/lib/project-gtc/table/project-gtc-table.tsx b/lib/project-gtc/table/project-gtc-table.tsx new file mode 100644 index 00000000..6e529ccf --- /dev/null +++ b/lib/project-gtc/table/project-gtc-table.tsx @@ -0,0 +1,100 @@ +"use client"; + +import * as React from "react"; +import { useRouter } from "next/navigation"; +import { DataTable } from "@/components/data-table/data-table"; +import { useDataTable } from "@/hooks/use-data-table"; +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"; +import type { + DataTableAdvancedFilterField, + DataTableRowAction, +} from "@/types/table" +import { getProjectGtcList } from "../service"; +import { getColumns } from "./project-gtc-table-columns"; +import { DeleteGtcFileDialog } from "./delete-gtc-file-dialog"; +import { UpdateGtcFileSheet } from "./update-gtc-file-sheet"; +import { ProjectGtcTableToolbarActions } from "./project-gtc-table-toolbar-actions"; +import { ProjectGtcView } from "@/db/schema"; + +interface ProjectGtcTableProps { + promises: Promise< + [ + Awaited<ReturnType<typeof getProjectGtcList>>, + ] + > +} + +export function ProjectGtcTable({ promises }: ProjectGtcTableProps) { + const router = useRouter(); + const [rowAction, setRowAction] = + React.useState<DataTableRowAction<ProjectGtcView> | null>(null) + + const [{ data, pageCount }] = + React.use(promises) + + // 컬럼 설정 - 외부 파일에서 가져옴 + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) + + // config 기반으로 필터 필드 설정 + const advancedFilterFields: DataTableAdvancedFilterField<ProjectGtcView>[] = [ + { id: "code", label: "프로젝트 코드", type: "text" }, + { id: "name", label: "프로젝트명", type: "text" }, + { + id: "type", label: "프로젝트 타입", type: "select", options: [ + { label: "Ship", value: "ship" }, + { label: "Offshore", value: "offshore" }, + { label: "Other", value: "other" }, + ] + }, + { id: "originalFileName", label: "GTC 파일명", type: "text" }, + { id: "projectCreatedAt", label: "프로젝트 생성일", type: "date" }, + { id: "gtcCreatedAt", label: "GTC 등록일", type: "date" }, + ]; + + const { table } = useDataTable({ + data, + columns, + pageCount, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "projectCreatedAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + <DataTable table={table}> + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + > + <ProjectGtcTableToolbarActions table={table} /> + </DataTableAdvancedToolbar> + </DataTable> + + <DeleteGtcFileDialog + open={rowAction?.type === "delete"} + onOpenChange={() => setRowAction(null)} + projects={rowAction?.row.original ? [rowAction?.row.original] : []} + showTrigger={false} + onSuccess={() => { + router.refresh(); + }} + /> + + <UpdateGtcFileSheet + open={rowAction?.type === "upload"} + onOpenChange={() => setRowAction(null)} + project={rowAction && rowAction.type === "upload" ? rowAction.row.original : null} + /> + </> + ); +}
\ No newline at end of file diff --git a/lib/project-gtc/table/update-gtc-file-sheet.tsx b/lib/project-gtc/table/update-gtc-file-sheet.tsx new file mode 100644 index 00000000..65a6bb45 --- /dev/null +++ b/lib/project-gtc/table/update-gtc-file-sheet.tsx @@ -0,0 +1,222 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import * as z from "zod" +import { Upload } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { + Form, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { uploadProjectGtcFile } from "../service" +import type { ProjectGtcView } from "@/db/schema" + +const updateProjectSchema = z.object({ + gtcFile: z.instanceof(File).optional(), +}) + +type UpdateProjectFormValues = z.infer<typeof updateProjectSchema> + +interface UpdateGtcFileSheetProps { + project: ProjectGtcView | null + open: boolean + onOpenChange: (open: boolean) => void +} + +export function UpdateGtcFileSheet({ + project, + open, + onOpenChange, +}: UpdateGtcFileSheetProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [selectedFile, setSelectedFile] = React.useState<File | null>(null) + + const form = useForm<UpdateProjectFormValues>({ + resolver: zodResolver(updateProjectSchema), + defaultValues: { + gtcFile: undefined, + }, + }) + + // 기존 값 세팅 (프로젝트 변경 시) + React.useEffect(() => { + if (project) { + form.reset({ + gtcFile: undefined, + }) + setSelectedFile(null) + } + }, [project, form]) + + // 파일 선택 처리 + const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => { + const file = event.target.files?.[0] + if (file) { + // PDF 파일만 허용 + if (file.type !== 'application/pdf') { + toast.error("PDF 파일만 업로드 가능합니다.") + return + } + setSelectedFile(file) + form.setValue("gtcFile", file) + } + } + + // 폼 제출 핸들러 + async function onSubmit(data: UpdateProjectFormValues) { + if (!project) { + toast.error("프로젝트 정보를 찾을 수 없습니다.") + return + } + + setIsLoading(true) + try { + // GTC 파일이 있으면 업로드 + if (data.gtcFile) { + const fileResult = await uploadProjectGtcFile(project.id, data.gtcFile) + if (!fileResult.success) { + toast.error(fileResult.error || "GTC 파일 업로드에 실패했습니다.") + return + } + toast.success("GTC 파일이 성공적으로 업로드되었습니다.") + } else { + toast.info("변경사항이 없습니다.") + } + + form.reset() + setSelectedFile(null) + onOpenChange(false) + } catch (error) { + console.error("GTC 파일 업로드 오류:", error) + toast.error("GTC 파일 업로드 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + if (!project) return null + + return ( + <Sheet open={open} onOpenChange={onOpenChange}> + <SheetContent className="flex flex-col gap-6 sm:max-w-xl"> + <SheetHeader className="text-left"> + <SheetTitle>GTC 파일 수정</SheetTitle> + <SheetDescription> + 프로젝트 정보는 수정할 수 없으며, GTC 파일만 업로드할 수 있습니다. + </SheetDescription> + </SheetHeader> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4"> + {/* 프로젝트 정보 (읽기 전용) */} + <div className="space-y-4"> + <div> + <FormLabel>프로젝트 코드</FormLabel> + <Input + value={project.code} + disabled + className="bg-muted" + /> + </div> + <div> + <FormLabel>프로젝트명</FormLabel> + <Input + value={project.name} + disabled + className="bg-muted" + /> + </div> + <div> + <FormLabel>프로젝트 타입</FormLabel> + <Input + value={project.type} + disabled + className="bg-muted" + /> + </div> + </div> + + {/* GTC 파일 업로드 */} + <FormField + control={form.control} + name="gtcFile" + render={() => ( + <FormItem> + <FormLabel>GTC 파일 (PDF만, 선택 시 기존 파일 교체)</FormLabel> + <div className="space-y-2"> + <label + htmlFor="gtc-file-input" + className="flex flex-col items-center justify-center w-full min-h-[8rem] border-2 border-dashed border-gray-300 rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100" + > + <div className="flex flex-col items-center justify-center p-4 text-center"> + <Upload className="w-8 h-8 mb-2 text-gray-500" /> + <span className="mb-1 text-base font-semibold text-gray-800"> + {selectedFile + ? selectedFile.name + : project.originalFileName + ? `현재 파일: ${project.originalFileName}` + : "현재 파일 없음"} + </span> + {selectedFile ? ( + <span className="text-xs text-gray-500"> + ({(selectedFile.size / 1024 / 1024).toFixed(2)} MB) + </span> + ) : ( + <> + <p className="mb-2 text-sm text-gray-500"> + 또는 클릭하여 파일을 선택하세요 + </p> + <p className="text-xs text-gray-500"> + PDF 파일만 + </p> + </> + )} + </div> + <input + id="gtc-file-input" + type="file" + className="hidden" + accept=".pdf" + onChange={handleFileSelect} + disabled={isLoading} + /> + </label> + </div> + <FormMessage /> + </FormItem> + )} + /> + <SheetFooter className="gap-2 pt-2 sm:space-x-0"> + <Button + type="button" + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button type="submit" disabled={isLoading || !selectedFile}> + {isLoading ? "업로드 중..." : "GTC 파일 업로드"} + </Button> + </SheetFooter> + </form> + </Form> + </SheetContent> + </Sheet> + ) +}
\ No newline at end of file diff --git a/lib/project-gtc/table/view-gtc-file-dialog.tsx b/lib/project-gtc/table/view-gtc-file-dialog.tsx new file mode 100644 index 00000000..f8cfecd9 --- /dev/null +++ b/lib/project-gtc/table/view-gtc-file-dialog.tsx @@ -0,0 +1,230 @@ +"use client" + +import * as React from "react" +import { Download, FileText, Calendar, HardDrive } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { format } from "date-fns" +import { ko } from "date-fns/locale" +import type { ProjectGtcView } from "@/db/schema" + +interface ViewGtcFileDialogProps { + project: ProjectGtcView | null + open: boolean + onOpenChange: (open: boolean) => void +} + +// 파일 크기 포맷팅 함수 +function formatBytes(bytes: number | null): string { + if (!bytes) return "0 B" + + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} + +export function ViewGtcFileDialog({ + project, + open, + onOpenChange, +}: ViewGtcFileDialogProps) { + if (!project || !project.gtcFileId) return null + + const handleDownload = async () => { + try { + // API를 통해 파일 다운로드 + const response = await fetch(`/api/project-gtc?action=download&projectId=${project.id}`, { + method: 'GET', + }); + + if (!response.ok) { + throw new Error('파일 다운로드에 실패했습니다.'); + } + + // 파일 blob 생성 + const blob = await response.blob(); + + // 다운로드 링크 생성 + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = project.originalFileName || 'gtc-file'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // 메모리 정리 + window.URL.revokeObjectURL(url); + } catch (error) { + console.error("파일 다운로드 오류:", error); + } + } + + const handlePreview = async () => { + try { + // API를 통해 파일 다운로드 + const response = await fetch(`/api/project-gtc?action=download&projectId=${project.id}`, { + method: 'GET', + }); + + if (!response.ok) { + throw new Error('파일을 열 수 없습니다.'); + } + + // 파일 blob 생성 + const blob = await response.blob(); + + // PDF 파일인 경우 새 탭에서 열기 + if (project.mimeType === 'application/pdf') { + const url = window.URL.createObjectURL(blob); + window.open(url, '_blank'); + // 메모리 정리는 브라우저가 탭을 닫을 때 자동으로 처리됨 + } else { + // 다른 파일 타입은 다운로드 + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = project.originalFileName || 'gtc-file'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } + } catch (error) { + console.error("파일 미리보기 오류:", error); + } + } + + const getFileIcon = () => { + if (project.mimeType?.includes('pdf')) { + return "📄" + } else if (project.mimeType?.includes('word') || project.mimeType?.includes('document')) { + return "📝" + } else if (project.mimeType?.includes('text')) { + return "📃" + } + return "📎" + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[500px]"> + <DialogHeader> + <DialogTitle>GTC 파일 정보</DialogTitle> + <DialogDescription> + 프로젝트 "{project.name}" ({project.code})의 GTC 파일 정보입니다. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {/* 프로젝트 정보 */} + <div className="p-4 bg-muted rounded-lg"> + <h4 className="font-medium mb-2">프로젝트 정보</h4> + <div className="space-y-2 text-sm"> + <div className="flex justify-between"> + <span className="text-muted-foreground">프로젝트 코드:</span> + <span className="font-medium">{project.code}</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">프로젝트명:</span> + <span className="font-medium">{project.name}</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">프로젝트 타입:</span> + <Badge variant="secondary">{project.type}</Badge> + </div> + </div> + </div> + + {/* 파일 정보 */} + <div className="p-4 bg-muted rounded-lg"> + <h4 className="font-medium mb-2">파일 정보</h4> + <div className="space-y-3"> + <div className="flex items-center space-x-3"> + <span className="text-2xl">{getFileIcon()}</span> + <div className="flex-1"> + <div className="font-medium">{project.originalFileName}</div> + <div className="text-sm text-muted-foreground"> + {project.fileName} + </div> + </div> + </div> + + <div className="grid grid-cols-2 gap-4 text-sm"> + <div className="flex items-center space-x-2"> + <HardDrive className="h-4 w-4 text-muted-foreground" /> + <span className="text-muted-foreground">파일 크기:</span> + <span className="font-medium"> + {project.fileSize ? formatBytes(project.fileSize) : '알 수 없음'} + </span> + </div> + <div className="flex items-center space-x-2"> + <FileText className="h-4 w-4 text-muted-foreground" /> + <span className="text-muted-foreground">파일 타입:</span> + <span className="font-medium"> + {project.mimeType || '알 수 없음'} + </span> + </div> + <div className="flex items-center space-x-2"> + <Calendar className="h-4 w-4 text-muted-foreground" /> + <span className="text-muted-foreground">업로드일:</span> + <span className="font-medium"> + {project.gtcCreatedAt ? + format(new Date(project.gtcCreatedAt), "yyyy-MM-dd HH:mm", { locale: ko }) : + '알 수 없음' + } + </span> + </div> + <div className="flex items-center space-x-2"> + <Calendar className="h-4 w-4 text-muted-foreground" /> + <span className="text-muted-foreground">수정일:</span> + <span className="font-medium"> + {project.gtcUpdatedAt ? + format(new Date(project.gtcUpdatedAt), "yyyy-MM-dd HH:mm", { locale: ko }) : + '알 수 없음' + } + </span> + </div> + </div> + </div> + </div> + </div> + + <DialogFooter className="flex space-x-2"> + <Button + type="button" + variant="outline" + onClick={() => onOpenChange(false)} + > + 닫기 + </Button> + <Button + type="button" + variant="outline" + onClick={handlePreview} + > + 미리보기 + </Button> + <Button + type="button" + onClick={handleDownload} + > + <Download className="mr-2 h-4 w-4" /> + 다운로드 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file diff --git a/lib/project-gtc/validations.ts b/lib/project-gtc/validations.ts new file mode 100644 index 00000000..963ffdd4 --- /dev/null +++ b/lib/project-gtc/validations.ts @@ -0,0 +1,32 @@ +import * as z from "zod" +import { createSearchParamsCache, + parseAsArrayOf, + parseAsInteger, + parseAsString, + parseAsStringEnum +} from "nuqs/server" + +import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers" +import { ProjectGtcView } from "@/db/schema" + +export const projectGtcSearchParamsSchema = createSearchParamsCache({ + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + sort: getSortingStateParser<ProjectGtcView>().withDefault([ + { id: "projectCreatedAt", desc: true }, + ]), + + // advanced filter + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + search: parseAsString.withDefault(""), +}) + +export const projectGtcFileSchema = z.object({ + projectId: z.number().min(1, "프로젝트 ID는 필수입니다."), + file: z.instanceof(File).refine((file) => file.size > 0, "파일은 필수입니다."), +}) + +export type ProjectGtcSearchParams = Awaited<ReturnType<typeof projectGtcSearchParamsSchema.parse>> +export type ProjectGtcFileInput = z.infer<typeof projectGtcFileSchema>
\ No newline at end of file diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts index c3c14aff..96d6a3c9 100644 --- a/lib/techsales-rfq/service.ts +++ b/lib/techsales-rfq/service.ts @@ -11,7 +11,7 @@ import { techSalesRfqItems, biddingProjects } from "@/db/schema"; -import { and, desc, eq, ilike, or, sql, inArray } from "drizzle-orm"; +import { and, desc, eq, ilike, or, sql, inArray, count, asc } from "drizzle-orm"; import { unstable_cache } from "@/lib/unstable-cache"; import { filterColumns } from "@/lib/filter-columns"; import { getErrorMessage } from "@/lib/handle-error"; @@ -3022,9 +3022,9 @@ export async function searchTechVendors(searchTerm: string, limit = 100, rfqType try { // RFQ 타입에 따른 벤더 타입 매핑 - const vendorTypeFilter = rfqType === "SHIP" ? "SHIP" : - rfqType === "TOP" ? "OFFSHORE_TOP" : - rfqType === "HULL" ? "OFFSHORE_HULL" : null; + const vendorTypeFilter = rfqType === "SHIP" ? "조선" : + rfqType === "TOP" ? "해양TOP" : + rfqType === "HULL" ? "해양HULL" : null; const whereConditions = [ eq(techVendors.status, "ACTIVE"), @@ -3034,9 +3034,9 @@ export async function searchTechVendors(searchTerm: string, limit = 100, rfqType ) ]; - // RFQ 타입이 지정된 경우 벤더 타입 필터링 추가 + // RFQ 타입이 지정된 경우 벤더 타입 필터링 추가 (컴마 구분 문자열에서 검색) if (vendorTypeFilter) { - whereConditions.push(eq(techVendors.techVendorType, vendorTypeFilter)); + whereConditions.push(sql`${techVendors.techVendorType} LIKE ${'%' + vendorTypeFilter + '%'}`); } const results = await db @@ -3058,4 +3058,237 @@ export async function searchTechVendors(searchTerm: string, limit = 100, rfqType console.error("Error searching tech vendors:", err); throw new Error(getErrorMessage(err)); } +} + +/** + * Accepted 상태의 Tech Sales Vendor Quotations 조회 (RFQ, Vendor 정보 포함) + */ +export async function getAcceptedTechSalesVendorQuotations(input: { + search?: string; + filters?: Filter<typeof techSalesVendorQuotations>[]; + sort?: { id: string; desc: boolean }[]; + page: number; + perPage: number; + rfqType?: "SHIP" | "TOP" | "HULL"; +}) { + unstable_noStore(); + + try { + const offset = (input.page - 1) * input.perPage; + + // 기본 WHERE 조건: status = 'Accepted'만 조회 + const baseConditions = [ + eq(techSalesVendorQuotations.status, 'Accepted') + ]; + + // 검색 조건 추가 + const searchConditions = []; + if (input.search) { + searchConditions.push( + ilike(techSalesRfqs.rfqCode, `%${input.search}%`), + ilike(techSalesRfqs.description, `%${input.search}%`), + ilike(sql`vendors.vendor_name`, `%${input.search}%`), + ilike(sql`vendors.vendor_code`, `%${input.search}%`) + ); + } + + // 정렬 조건 변환 + const orderByConditions: OrderByType[] = []; + if (input.sort?.length) { + input.sort.forEach((sortItem) => { + switch (sortItem.id) { + case "rfqCode": + orderByConditions.push(sortItem.desc ? desc(techSalesRfqs.rfqCode) : asc(techSalesRfqs.rfqCode)); + break; + case "description": + orderByConditions.push(sortItem.desc ? desc(techSalesRfqs.description) : asc(techSalesRfqs.description)); + break; + case "vendorName": + orderByConditions.push(sortItem.desc ? desc(sql`vendors.vendor_name`) : asc(sql`vendors.vendor_name`)); + break; + case "vendorCode": + orderByConditions.push(sortItem.desc ? desc(sql`vendors.vendor_code`) : asc(sql`vendors.vendor_code`)); + break; + case "totalPrice": + orderByConditions.push(sortItem.desc ? desc(techSalesVendorQuotations.totalPrice) : asc(techSalesVendorQuotations.totalPrice)); + break; + case "acceptedAt": + orderByConditions.push(sortItem.desc ? desc(techSalesVendorQuotations.acceptedAt) : asc(techSalesVendorQuotations.acceptedAt)); + break; + default: + orderByConditions.push(desc(techSalesVendorQuotations.acceptedAt)); + } + }); + } else { + orderByConditions.push(desc(techSalesVendorQuotations.acceptedAt)); + } + + // 필터 조건 추가 + const filterConditions = []; + if (input.filters?.length) { + const { filterWhere, joinOperator } = filterColumns({ + table: techSalesVendorQuotations, + filters: input.filters, + joinOperator: input.joinOperator ?? "and", + }); + if (filterWhere) { + filterConditions.push(filterWhere); + } + } + + // RFQ 타입 필터 + if (input.rfqType) { + filterConditions.push(eq(techSalesRfqs.rfqType, input.rfqType)); + } + + // 모든 조건 결합 + const allConditions = [ + ...baseConditions, + ...filterConditions, + ...(searchConditions.length > 0 ? [or(...searchConditions)] : []) + ]; + + const whereCondition = allConditions.length > 1 + ? and(...allConditions) + : allConditions[0]; + + // 데이터 조회 + const data = await db + .select({ + // Quotation 정보 + id: techSalesVendorQuotations.id, + rfqId: techSalesVendorQuotations.rfqId, + vendorId: techSalesVendorQuotations.vendorId, + quotationCode: techSalesVendorQuotations.quotationCode, + quotationVersion: techSalesVendorQuotations.quotationVersion, + totalPrice: techSalesVendorQuotations.totalPrice, + currency: techSalesVendorQuotations.currency, + validUntil: techSalesVendorQuotations.validUntil, + status: techSalesVendorQuotations.status, + remark: techSalesVendorQuotations.remark, + submittedAt: techSalesVendorQuotations.submittedAt, + acceptedAt: techSalesVendorQuotations.acceptedAt, + createdAt: techSalesVendorQuotations.createdAt, + updatedAt: techSalesVendorQuotations.updatedAt, + + // RFQ 정보 + rfqCode: techSalesRfqs.rfqCode, + rfqType: techSalesRfqs.rfqType, + description: techSalesRfqs.description, + dueDate: techSalesRfqs.dueDate, + rfqStatus: techSalesRfqs.status, + materialCode: techSalesRfqs.materialCode, + + // Vendor 정보 + vendorName: sql<string>`vendors.vendor_name`, + vendorCode: sql<string | null>`vendors.vendor_code`, + vendorEmail: sql<string | null>`vendors.email`, + vendorCountry: sql<string | null>`vendors.country`, + + // Project 정보 + projNm: biddingProjects.projNm, + pspid: biddingProjects.pspid, + sector: biddingProjects.sector, + }) + .from(techSalesVendorQuotations) + .leftJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id)) + .leftJoin(sql`vendors`, eq(techSalesVendorQuotations.vendorId, sql`vendors.id`)) + .leftJoin(biddingProjects, eq(techSalesRfqs.biddingProjectId, biddingProjects.id)) + .where(whereCondition) + .orderBy(...orderByConditions) + .limit(input.perPage) + .offset(offset); + + // 총 개수 조회 + const totalCount = await db + .select({ count: count() }) + .from(techSalesVendorQuotations) + .leftJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id)) + .leftJoin(sql`vendors`, eq(techSalesVendorQuotations.vendorId, sql`vendors.id`)) + .leftJoin(biddingProjects, eq(techSalesRfqs.biddingProjectId, biddingProjects.id)) + .where(whereCondition); + + const total = totalCount[0]?.count ?? 0; + const pageCount = Math.ceil(total / input.perPage); + + return { + data, + pageCount, + total, + }; + + } catch (error) { + console.error("getAcceptedTechSalesVendorQuotations 오류:", error); + throw new Error(`Accepted quotations 조회 실패: ${getErrorMessage(error)}`); + } +} + +/** + * 벤더 견적서 거절 처리 (벤더가 직접 거절) + */ +export async function rejectTechSalesVendorQuotations(input: { + quotationIds: number[]; + rejectionReason?: string; +}) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + throw new Error("인증이 필요합니다."); + } + + const result = await db.transaction(async (tx) => { + // 견적서들이 존재하고 벤더가 권한이 있는지 확인 + const quotations = await tx + .select({ + id: techSalesVendorQuotations.id, + status: techSalesVendorQuotations.status, + vendorId: techSalesVendorQuotations.vendorId, + }) + .from(techSalesVendorQuotations) + .where(inArray(techSalesVendorQuotations.id, input.quotationIds)); + + if (quotations.length !== input.quotationIds.length) { + throw new Error("일부 견적서를 찾을 수 없습니다."); + } + + // 이미 거절된 견적서가 있는지 확인 + const alreadyRejected = quotations.filter(q => q.status === "Rejected"); + if (alreadyRejected.length > 0) { + throw new Error("이미 거절된 견적서가 포함되어 있습니다."); + } + + // 승인된 견적서가 있는지 확인 + const alreadyAccepted = quotations.filter(q => q.status === "Accepted"); + if (alreadyAccepted.length > 0) { + throw new Error("이미 승인된 견적서는 거절할 수 없습니다."); + } + + // 견적서 상태를 거절로 변경 + await tx + .update(techSalesVendorQuotations) + .set({ + status: "Rejected", + rejectionReason: input.rejectionReason || null, + updatedBy: parseInt(session.user.id), + updatedAt: new Date(), + }) + .where(inArray(techSalesVendorQuotations.id, input.quotationIds)); + + return { success: true, updatedCount: quotations.length }; + }); + revalidateTag("techSalesRfqs"); + revalidateTag("techSalesVendorQuotations"); + revalidatePath("/partners/techsales/rfq-ship", "page"); + return { + success: true, + message: `${result.updatedCount}개의 견적서가 거절되었습니다.`, + data: result + }; + } catch (error) { + console.error("견적서 거절 오류:", error); + return { + success: false, + error: getErrorMessage(error) + }; + } }
\ No newline at end of file diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx index ddee2317..b89f8953 100644 --- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx +++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx @@ -15,7 +15,8 @@ import { } from "@/components/ui/tooltip" import { TechSalesVendorQuotations, - TECH_SALES_QUOTATION_STATUS_CONFIG + TECH_SALES_QUOTATION_STATUS_CONFIG, + TECH_SALES_QUOTATION_STATUSES } from "@/db/schema" import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" @@ -70,14 +71,21 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge className="translate-y-0.5" /> ), - cell: ({ row }) => ( - <Checkbox - checked={row.getIsSelected()} - onCheckedChange={(value) => row.toggleSelected(!!value)} + cell: ({ row }) => { + const isRejected = row.original.status === TECH_SALES_QUOTATION_STATUSES.REJECTED; + const isAccepted = row.original.status === TECH_SALES_QUOTATION_STATUSES.ACCEPTED; + const isDisabled = isRejected || isAccepted; + + return ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} aria-label="행 선택" - className="translate-y-0.5" - /> - ), + className="translate-y-0.5" + disabled={isDisabled} + /> + ); + }, enableSorting: false, enableHiding: false, }, @@ -158,33 +166,33 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge // enableSorting: true, // enableHiding: true, // }, - { - accessorKey: "itemName", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="자재명" /> - ), - cell: ({ row }) => { - const itemName = row.getValue("itemName") as string; - return ( - <div className="min-w-48 max-w-64"> - <TooltipProvider> - <Tooltip> - <TooltipTrigger asChild> - <span className="truncate block text-sm"> - {itemName || "N/A"} - </span> - </TooltipTrigger> - <TooltipContent> - <p className="max-w-xs">{itemName || "N/A"}</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </div> - ); - }, - enableSorting: true, - enableHiding: true, - }, + // { + // accessorKey: "itemName", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="자재명" /> + // ), + // cell: ({ row }) => { + // const itemName = row.getValue("itemName") as string; + // return ( + // <div className="min-w-48 max-w-64"> + // <TooltipProvider> + // <Tooltip> + // <TooltipTrigger asChild> + // <span className="truncate block text-sm"> + // {itemName || "N/A"} + // </span> + // </TooltipTrigger> + // <TooltipContent> + // <p className="max-w-xs">{itemName || "N/A"}</p> + // </TooltipContent> + // </Tooltip> + // </TooltipProvider> + // </div> + // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, { accessorKey: "projNm", header: ({ column }) => ( @@ -597,6 +605,9 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge const quotation = row.original; const rfqCode = quotation.rfqCode || "N/A"; const tooltipText = `${rfqCode} 견적서 작성`; + const isRejected = quotation.status === "Rejected"; + const isAccepted = quotation.status === "Accepted"; + const isDisabled = isRejected || isAccepted; return ( <div className="w-16"> @@ -607,16 +618,19 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: Ge variant="ghost" size="icon" onClick={() => { - router.push(`/ko/partners/techsales/rfq-ship/${quotation.id}`); + if (!isDisabled) { + router.push(`/ko/partners/techsales/rfq-ship/${quotation.id}`); + } }} className="h-8 w-8" + disabled={isDisabled} > <Edit className="h-4 w-4" /> <span className="sr-only">견적서 작성</span> </Button> </TooltipTrigger> <TooltipContent> - <p>{tooltipText}</p> + <p>{isRejected ? "거절된 견적서는 편집할 수 없습니다" : isAccepted ? "승인된 견적서는 편집할 수 없습니다" : tooltipText}</p> </TooltipContent> </Tooltip> </TooltipProvider> diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx index 55dcad92..5e5d4f39 100644 --- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx +++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx @@ -12,9 +12,24 @@ import { useRouter } from "next/navigation" import { getColumns } from "./vendor-quotations-table-columns" import { TechSalesRfqAttachmentsSheet, ExistingTechSalesAttachment } from "../../table/tech-sales-rfq-attachments-sheet" import { RfqItemsViewDialog } from "../../table/rfq-items-view-dialog" -import { getTechSalesRfqAttachments, getVendorQuotations } from "@/lib/techsales-rfq/service" +import { getTechSalesRfqAttachments, getVendorQuotations, rejectTechSalesVendorQuotations } from "@/lib/techsales-rfq/service" import { toast } from "sonner" import { Skeleton } from "@/components/ui/skeleton" +import { Button } from "@/components/ui/button" +import { X } from "lucide-react" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" interface QuotationWithRfqCode extends TechSalesVendorQuotations { rfqCode?: string | null; @@ -95,8 +110,6 @@ function TableLoadingSkeleton() { ) } - - export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTableProps) { const searchParams = useSearchParams() const router = useRouter() @@ -110,6 +123,11 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) const [selectedRfqForItems, setSelectedRfqForItems] = React.useState<{ id: number; rfqCode?: string; status?: string; rfqType?: "SHIP" | "TOP" | "HULL"; } | null>(null) + // 거절 다이얼로그 상태 + const [rejectDialogOpen, setRejectDialogOpen] = React.useState(false) + const [rejectionReason, setRejectionReason] = React.useState("") + const [isRejecting, setIsRejecting] = React.useState(false) + // 데이터 로딩 상태 const [data, setData] = React.useState<QuotationWithRfqCode[]>([]) const [pageCount, setPageCount] = React.useState(0) @@ -248,6 +266,54 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab setSelectedRfqForItems(rfq) setItemsDialogOpen(true) }, []) + + // 거절 처리 함수 + const handleRejectQuotations = React.useCallback(async () => { + if (!table) return; + + const selectedRows = table.getFilteredSelectedRowModel().rows; + const quotationIds = selectedRows.map(row => row.original.id); + + if (quotationIds.length === 0) { + toast.error("거절할 견적서를 선택해주세요."); + return; + } + + // 거절할 수 없는 상태의 견적서가 있는지 확인 + const invalidStatuses = selectedRows.filter(row => + row.original.status === "Accepted" || row.original.status === "Rejected" + ); + + if (invalidStatuses.length > 0) { + toast.error("이미 승인되었거나 거절된 견적서는 거절할 수 없습니다."); + return; + } + + setIsRejecting(true); + + try { + const result = await rejectTechSalesVendorQuotations({ + quotationIds, + rejectionReason: rejectionReason.trim() || undefined, + }); + + if (result.success) { + toast.success(result.message); + setRejectDialogOpen(false); + setRejectionReason(""); + table.resetRowSelection(); + // 데이터 다시 로드 + await loadData(); + } else { + toast.error(result.error || "견적서 거절 중 오류가 발생했습니다."); + } + } catch (error) { + console.error("견적서 거절 오류:", error); + toast.error("견적서 거절 중 오류가 발생했습니다."); + } finally { + setIsRejecting(false); + } + }, [rejectionReason, loadData]); // 테이블 컬럼 정의 const columns = React.useMemo(() => getColumns({ @@ -322,6 +388,7 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab enableAdvancedFilter: true, enableColumnResizing: true, columnResizeMode: 'onChange', + enableRowSelection: true, // 행 선택 활성화 initialState: { sorting: initialSettings.sort, columnPinning: { right: ["actions"] }, @@ -366,6 +433,48 @@ export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTab filterFields={advancedFilterFields} shallow={false} > + {/* 선택된 행이 있을 때 거절 버튼 표시 */} + {table && table.getFilteredSelectedRowModel().rows.length > 0 && ( + <AlertDialog open={rejectDialogOpen} onOpenChange={setRejectDialogOpen}> + <AlertDialogTrigger asChild> + <Button variant="destructive" size="sm"> + <X className="mr-2 h-4 w-4" /> + 선택한 견적서 거절 ({table.getFilteredSelectedRowModel().rows.length}개) + </Button> + </AlertDialogTrigger> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>견적서 거절</AlertDialogTitle> + <AlertDialogDescription> + 선택한 {table.getFilteredSelectedRowModel().rows.length}개의 견적서를 거절하시겠습니까? + 거절된 견적서는 다시 되돌릴 수 없습니다. + </AlertDialogDescription> + </AlertDialogHeader> + <div className="grid gap-4 py-4"> + <div className="grid gap-2"> + <Label htmlFor="rejection-reason">거절 사유 (선택사항)</Label> + <Textarea + id="rejection-reason" + placeholder="거절 사유를 입력하세요..." + value={rejectionReason} + onChange={(e) => setRejectionReason(e.target.value)} + /> + </div> + </div> + <AlertDialogFooter> + <AlertDialogCancel>취소</AlertDialogCancel> + <AlertDialogAction + onClick={handleRejectQuotations} + disabled={isRejecting} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {isRejecting ? "처리 중..." : "거절"} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + )} + {!isInitialLoad && isLoading && ( <div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" /> |
