From e0dfb55c5457aec489fc084c4567e791b4c65eb1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 26 Mar 2025 00:37:41 +0000 Subject: 3/25 까지의 대표님 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rfqs/table/BudgetaryRfqSelector.tsx | 261 +++++++++ lib/rfqs/table/ItemsDialog.tsx | 744 ++++++++++++++++++++++++++ lib/rfqs/table/add-rfq-dialog.tsx | 349 ++++++++++++ lib/rfqs/table/attachment-rfq-sheet.tsx | 430 +++++++++++++++ lib/rfqs/table/delete-rfqs-dialog.tsx | 149 ++++++ lib/rfqs/table/feature-flags-provider.tsx | 108 ++++ lib/rfqs/table/feature-flags.tsx | 96 ++++ lib/rfqs/table/rfqs-table-columns.tsx | 315 +++++++++++ lib/rfqs/table/rfqs-table-floating-bar.tsx | 338 ++++++++++++ lib/rfqs/table/rfqs-table-toolbar-actions.tsx | 55 ++ lib/rfqs/table/rfqs-table.tsx | 264 +++++++++ lib/rfqs/table/update-rfq-sheet.tsx | 283 ++++++++++ 12 files changed, 3392 insertions(+) create mode 100644 lib/rfqs/table/BudgetaryRfqSelector.tsx create mode 100644 lib/rfqs/table/ItemsDialog.tsx create mode 100644 lib/rfqs/table/add-rfq-dialog.tsx create mode 100644 lib/rfqs/table/attachment-rfq-sheet.tsx create mode 100644 lib/rfqs/table/delete-rfqs-dialog.tsx create mode 100644 lib/rfqs/table/feature-flags-provider.tsx create mode 100644 lib/rfqs/table/feature-flags.tsx create mode 100644 lib/rfqs/table/rfqs-table-columns.tsx create mode 100644 lib/rfqs/table/rfqs-table-floating-bar.tsx create mode 100644 lib/rfqs/table/rfqs-table-toolbar-actions.tsx create mode 100644 lib/rfqs/table/rfqs-table.tsx create mode 100644 lib/rfqs/table/update-rfq-sheet.tsx (limited to 'lib/rfqs/table') diff --git a/lib/rfqs/table/BudgetaryRfqSelector.tsx b/lib/rfqs/table/BudgetaryRfqSelector.tsx new file mode 100644 index 00000000..cea53c1d --- /dev/null +++ b/lib/rfqs/table/BudgetaryRfqSelector.tsx @@ -0,0 +1,261 @@ +"use client" + +import * as React from "react" +import { Check, ChevronsUpDown, Loader } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command" +import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" +import { cn } from "@/lib/utils" +import { useDebounce } from "@/hooks/use-debounce" +import { getBudgetaryRfqs, type BudgetaryRfq } from "../service" + +interface BudgetaryRfqSelectorProps { + selectedRfqId?: number; + onRfqSelect: (rfq: BudgetaryRfq | null) => void; + placeholder?: string; +} + +export function BudgetaryRfqSelector({ + selectedRfqId, + onRfqSelect, + placeholder = "Budgetary RFQ 선택..." +}: BudgetaryRfqSelectorProps) { + const [searchTerm, setSearchTerm] = React.useState(""); + const debouncedSearchTerm = useDebounce(searchTerm, 300); + + const [open, setOpen] = React.useState(false); + const [loading, setLoading] = React.useState(false); + const [budgetaryRfqs, setBudgetaryRfqs] = React.useState([]); + const [selectedRfq, setSelectedRfq] = React.useState(null); + const [page, setPage] = React.useState(1); + const [hasMore, setHasMore] = React.useState(true); + const [totalCount, setTotalCount] = React.useState(0); + + const listRef = React.useRef(null); + + // 초기 선택된 RFQ가 있을 경우 로드 + React.useEffect(() => { + if (selectedRfqId && open) { + const loadSelectedRfq = async () => { + try { + const result = await getBudgetaryRfqs({ + limit: 1, + // null을 undefined로 변환하여 타입 오류 해결 + projectId: selectedRfq?.projectId ?? undefined + }); + + if ('rfqs' in result && result.rfqs) { + // 옵셔널 체이닝 또는 조건부 검사로 undefined 체크 + const foundRfq = result.rfqs.find(rfq => rfq.id === selectedRfqId); + if (foundRfq) { + setSelectedRfq(foundRfq); + } + } + } catch (error) { + console.error("선택된 RFQ 로드 오류:", error); + } + }; + + if (!selectedRfq || selectedRfq.id !== selectedRfqId) { + loadSelectedRfq(); + } + } + }, [selectedRfqId, open, selectedRfq]); + + // 검색어 변경 시 데이터 리셋 및 재로드 + React.useEffect(() => { + if (open) { + setPage(1); + setHasMore(true); + setBudgetaryRfqs([]); + loadBudgetaryRfqs(1, true); + } + }, [debouncedSearchTerm, open]); + + // 데이터 로드 함수 + const loadBudgetaryRfqs = async (pageToLoad: number, reset = false) => { + if (!open) return; + + setLoading(true); + try { + const limit = 20; // 한 번에 로드할 항목 수 + const result = await getBudgetaryRfqs({ + search: debouncedSearchTerm, + limit, + offset: (pageToLoad - 1) * limit, + }); + + if ('rfqs' in result && result.rfqs) { + if (reset) { + setBudgetaryRfqs(result.rfqs); + } else { + setBudgetaryRfqs(prev => [...prev, ...result.rfqs]); + } + + setTotalCount(result.totalCount); + setHasMore(result.rfqs.length === limit && (pageToLoad * limit) < result.totalCount); + setPage(pageToLoad); + } + } catch (error) { + console.error("Budgetary RFQs 로드 오류:", error); + } finally { + setLoading(false); + } + }; + + // 무한 스크롤 처리 + const handleScroll = () => { + if (listRef.current) { + const { scrollTop, scrollHeight, clientHeight } = listRef.current; + + // 스크롤이 90% 이상 내려갔을 때 다음 페이지 로드 + if (scrollTop + clientHeight >= scrollHeight * 0.9 && !loading && hasMore) { + loadBudgetaryRfqs(page + 1); + } + } + }; + + // RFQ를 프로젝트별로 그룹화하는 함수 + const groupRfqsByProject = (rfqs: BudgetaryRfq[]) => { + const groups: Record = {}; + + // 'No Project' 그룹 기본 생성 + groups['no-project'] = { + projectId: null, + projectCode: null, + projectName: null, + rfqs: [] + }; + + // 프로젝트별로 RFQ 그룹화 + rfqs.forEach(rfq => { + const key = rfq.projectId ? `project-${rfq.projectId}` : 'no-project'; + + if (!groups[key] && rfq.projectId) { + groups[key] = { + projectId: rfq.projectId, + projectCode: rfq.projectCode, + projectName: rfq.projectName, + rfqs: [] + }; + } + + groups[key].rfqs.push(rfq); + }); + + // 필터링된 결과가 있는 그룹만 남기기 + return Object.values(groups).filter(group => group.rfqs.length > 0); + }; + + // 그룹화된 RFQ 목록 + const groupedRfqs = React.useMemo(() => { + return groupRfqsByProject(budgetaryRfqs); + }, [budgetaryRfqs]); + + // RFQ 선택 처리 + const handleRfqSelect = (rfq: BudgetaryRfq | null) => { + setSelectedRfq(rfq); + onRfqSelect(rfq); + setOpen(false); + }; + + return ( + + + + + + + + + 검색 결과가 없습니다 + + + handleRfqSelect(null)} + > + + 선택 안함 + + + + {groupedRfqs.map((group, index) => ( + + {group.rfqs.map((rfq) => ( + handleRfqSelect(rfq)} + > + + {rfq.rfqCode || ""} + + - {rfq.description || ""} + + + ))} + + ))} + + {loading && ( +
+ +
+ )} + + {!loading && !hasMore && budgetaryRfqs.length > 0 && ( +
+ 총 {totalCount}개 중 {budgetaryRfqs.length}개 표시됨 +
+ )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/lib/rfqs/table/ItemsDialog.tsx b/lib/rfqs/table/ItemsDialog.tsx new file mode 100644 index 00000000..f1dbf90e --- /dev/null +++ b/lib/rfqs/table/ItemsDialog.tsx @@ -0,0 +1,744 @@ +"use client" + +import * as React from "react" +import { useForm, useFieldArray, useWatch } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@/components/ui/form" +import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" +import { + Command, + CommandInput, + CommandList, + CommandItem, + CommandGroup, + CommandEmpty +} from "@/components/ui/command" +import { Check, ChevronsUpDown, Plus, Trash2, Save, X, AlertCircle, Eye } from "lucide-react" +import { toast } from "sonner" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { Badge } from "@/components/ui/badge" + +import { createRfqItem, deleteRfqItem } from "../service" +import { RfqWithItemCount } from "@/db/schema/rfq" +import { RfqType } from "../validations" + +// Zod 스키마 - 수량은 string으로 받아서 나중에 변환 +const itemSchema = z.object({ + id: z.number().optional(), + itemCode: z.string().nonempty({ message: "아이템 코드를 선택해주세요" }), + description: z.string().optional(), + quantity: z.coerce.number().min(1, { message: "최소 수량은 1입니다" }).default(1), + uom: z.string().default("each"), +}); + +const itemsFormSchema = z.object({ + rfqId: z.number().int(), + items: z.array(itemSchema).min(1, { message: "최소 1개 이상의 아이템을 추가해주세요" }), +}); + +type ItemsFormSchema = z.infer; + +interface RfqsItemsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + rfq: RfqWithItemCount | null; + defaultItems?: { + id?: number; + itemCode: string; + quantity?: number | null; + description?: string | null; + uom?: string | null; + }[]; + itemsList: { code: string | null; name: string }[]; + rfqType?: RfqType; +} + +export function RfqsItemsDialog({ + open, + onOpenChange, + rfq, + defaultItems = [], + itemsList, + rfqType +}: RfqsItemsDialogProps) { + const rfqId = rfq?.rfqId ?? 0; + + // 편집 가능 여부 확인 - DRAFT 상태일 때만 편집 가능 + const isEditable = rfq?.status === "DRAFT"; + + // 초기 아이템 ID 목록을 추적하기 위한 상태 추가 + const [initialItemIds, setInitialItemIds] = React.useState<(number | undefined)[]>([]); + + // 삭제된 아이템 ID를 저장하는 상태 추가 + const [deletedItemIds, setDeletedItemIds] = React.useState([]); + + // 1) form + const form = useForm({ + resolver: zodResolver(itemsFormSchema), + defaultValues: { + rfqId, + items: defaultItems.length > 0 ? defaultItems.map((it) => ({ + id: it.id, + quantity: it.quantity ?? 1, + uom: it.uom ?? "each", + itemCode: it.itemCode ?? "", + description: it.description ?? "", + })) : [{ itemCode: "", description: "", quantity: 1, uom: "each" }], + }, + mode: "onChange", // 입력 필드가 변경될 때마다 유효성 검사 + }); + + // 다이얼로그가 열릴 때마다 폼 초기화 및 초기 아이템 ID 저장 + React.useEffect(() => { + if (open) { + const initialItems = defaultItems.length > 0 + ? defaultItems.map((it) => ({ + id: it.id, + quantity: it.quantity ?? 1, + uom: it.uom ?? "each", + itemCode: it.itemCode ?? "", + description: it.description ?? "", + })) + : [{ itemCode: "", description: "", quantity: 1, uom: "each" }]; + + form.reset({ + rfqId, + items: initialItems, + }); + + // 초기 아이템 ID 목록 저장 + setInitialItemIds(defaultItems.map(item => item.id)); + + // 삭제된 아이템 목록 초기화 + setDeletedItemIds([]); + setHasUnsavedChanges(false); + } + }, [open, defaultItems, rfqId, form]); + + // 새로운 요소에 대한 ref 배열 + const inputRefs = React.useRef>([]); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false); + const [isExitDialogOpen, setIsExitDialogOpen] = React.useState(false); + + // 폼 변경 감지 - 편집 가능한 경우에만 변경 감지 + React.useEffect(() => { + if (!isEditable) return; + + const subscription = form.watch(() => { + setHasUnsavedChanges(true); + }); + return () => subscription.unsubscribe(); + }, [form, isEditable]); + + // 2) field array + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "items", + }); + + // 3) watch items array + const watchItems = form.watch("items"); + + // 4) Add item row with auto-focus + function handleAddItem() { + if (!isEditable) return; + + // 명시적으로 숫자 타입으로 지정 + append({ + itemCode: "", + description: "", + quantity: 1, + uom: "each" + }); + setHasUnsavedChanges(true); + + // 다음 렌더링 사이클에서 새로 추가된 항목에 포커스 + setTimeout(() => { + const newIndex = fields.length; + const button = inputRefs.current[newIndex]; + if (button) { + button.click(); + } + }, 100); + } + + // 항목 직접 삭제 - 기존 ID가 있을 경우 삭제 목록에 추가 + const handleRemoveItem = (index: number) => { + if (!isEditable) return; + + const itemToRemove = form.getValues().items[index]; + + // 기존 ID가 있는 아이템이라면 삭제 목록에 추가 + if (itemToRemove.id !== undefined) { + setDeletedItemIds(prev => [...prev, itemToRemove.id as number]); + } + + remove(index); + setHasUnsavedChanges(true); + + // 포커스 처리: 다음 항목이 있으면 다음 항목으로, 없으면 마지막 항목으로 + setTimeout(() => { + const nextIndex = Math.min(index, fields.length - 1); + if (nextIndex >= 0 && inputRefs.current[nextIndex]) { + inputRefs.current[nextIndex]?.click(); + } + }, 50); + }; + + // 다이얼로그 닫기 전 확인 + const handleDialogClose = (open: boolean) => { + if (!open && hasUnsavedChanges && isEditable) { + setIsExitDialogOpen(true); + } else { + onOpenChange(open); + } + }; + + // 필드 포커스 유틸리티 함수 + const focusField = (selector: string) => { + if (!isEditable) return; + + setTimeout(() => { + const element = document.querySelector(selector) as HTMLInputElement | null; + if (element) { + element.focus(); + } + }, 10); + }; + + // 5) Submit - 업데이트된 제출 로직 (생성/수정 + 삭제 처리) + async function onSubmit(data: ItemsFormSchema) { + if (!isEditable) return; + + try { + setIsSubmitting(true); + + // 각 아이템이 유효한지 확인 + const anyInvalidItems = data.items.some(item => !item.itemCode || item.quantity < 1); + + if (anyInvalidItems) { + toast.error("유효하지 않은 아이템이 있습니다. 모든 필드를 확인해주세요."); + setIsSubmitting(false); + return; + } + + // 1. 삭제 처리 - 삭제된 아이템 ID가 있으면 삭제 요청 + const deletePromises = deletedItemIds.map(id => + deleteRfqItem({ + id: id, + rfqId: rfqId, + rfqType: rfqType ?? RfqType.PURCHASE + }) + ); + + // 2. 생성/수정 처리 - 폼에 남아있는 아이템들 + const upsertPromises = data.items.map((item) => + createRfqItem({ + rfqId: rfqId, + itemCode: item.itemCode, + description: item.description, + // 명시적으로 숫자로 변환 + quantity: Number(item.quantity), + uom: item.uom, + rfqType: rfqType ?? RfqType.PURCHASE, + id: item.id // 기존 ID가 있으면 업데이트, 없으면 생성 + }) + ); + + // 모든 요청 병렬 처리 + await Promise.all([...deletePromises, ...upsertPromises]); + + toast.success("RFQ 아이템이 성공적으로 저장되었습니다!"); + setHasUnsavedChanges(false); + onOpenChange(false); + } catch (err) { + toast.error(`오류가 발생했습니다: ${String(err)}`); + } finally { + setIsSubmitting(false); + } + } + + // 단축키 처리 - 편집 가능한 경우에만 단축키 활성화 + React.useEffect(() => { + if (!isEditable) return; + + const handleKeyDown = (e: KeyboardEvent) => { + // Alt+N: 새 항목 추가 + if (e.altKey && e.key === 'n') { + e.preventDefault(); + handleAddItem(); + } + // Ctrl+S: 저장 + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + form.handleSubmit(onSubmit)(); + } + // Esc: 포커스된 팝오버 닫기 + if (e.key === 'Escape') { + document.querySelectorAll('[role="combobox"][aria-expanded="true"]').forEach( + (el) => (el as HTMLButtonElement).click() + ); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [form, isEditable]); + + return ( + <> + + + + + {isEditable ? "RFQ 아이템 관리" : "RFQ 아이템 조회"} + + {rfq?.rfqCode || `RFQ #${rfqId}`} + + {rfqType && ( + + {rfqType === RfqType.PURCHASE ? "구매 RFQ" : "예산 RFQ"} + + )} + {rfq?.status && ( + + {rfq.status} + + )} + + + {isEditable + ? (rfq?.description || '아이템을 각 행에 하나씩 추가할 수 있습니다.') + : '드래프트 상태가 아닌 RFQ는 아이템을 편집할 수 없습니다.'} + + +
+
+ +
+ {/* 헤더 행 (라벨) */} +
+
아이템
+
설명
+
수량
+
단위
+ {isEditable &&
} +
+ + {/* 아이템 행들 */} +
+ {fields.map((field, index) => { + // 현재 row의 itemCode + const codeValue = watchItems[index]?.itemCode || ""; + // "이미" 사용된 코드를 모두 구함 + const usedCodes = watchItems + .map((it, i) => i === index ? null : it.itemCode) + .filter(Boolean) as string[]; + + // itemsList에서 "현재 선택한 code"만 예외적으로 허용하고, + // 다른 행에서 이미 사용한 code는 제거 + const filteredItems = (itemsList || []) + .filter((it) => { + if (!it.code) return false; + if (it.code === codeValue) return true; + return !usedCodes.includes(it.code); + }) + .map((it) => ({ + code: it.code ?? "", // fallback + name: it.name, + })); + + // 선택된 아이템 찾기 + const selected = filteredItems.find(it => it.code === codeValue); + + return ( +
+ {/* -- itemCode + Popover(Select) -- */} + {isEditable ? ( + { + const [popoverOpen, setPopoverOpen] = React.useState(false); + const selected = filteredItems.find(it => it.code === field.value); + + return ( + + + + + + + + + + + 아이템을 찾을 수 없습니다. + + {filteredItems.map((it) => { + const label = `${it.code} - ${it.name}`; + return ( + { + field.onChange(it.code); + setPopoverOpen(false); + // 자동으로 다음 필드로 포커스 이동 + focusField(`input[name="items.${index}.description"]`); + }} + > + {label} + + + ); + })} + + + + + + + {form.formState.errors.items?.[index]?.itemCode && ( + + )} + + ); + }} + /> + ) : ( +
+ {selected ? `${selected.code} - ${selected.name}` : codeValue} +
+ )} + + {/* ID 필드 추가 (숨김) */} + ( + + )} + /> + + {/* description */} + {isEditable ? ( + ( + + + { + if (e.key === 'Enter') { + e.preventDefault(); + focusField(`input[name="items.${index}.quantity"]`); + } + }} + /> + + + + )} + /> + ) : ( +
+ {watchItems[index]?.description || ""} +
+ )} + + {/* quantity */} + {isEditable ? ( + ( + + + { + const value = e.target.value === '' ? 1 : parseInt(e.target.value, 10); + field.onChange(isNaN(value) ? 1 : value); + }} + // 최소값 보장 (빈 문자열 방지) + onBlur={(e) => { + if (e.target.value === '' || parseInt(e.target.value, 10) < 1) { + field.onChange(1); + } + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + focusField(`input[name="items.${index}.uom"]`); + } + }} + /> + + {form.formState.errors.items?.[index]?.quantity && ( + + )} + + )} + /> + ) : ( +
+ {watchItems[index]?.quantity} +
+ )} + + {/* uom */} + {isEditable ? ( + ( + + + { + if (e.key === 'Enter') { + e.preventDefault(); + // 마지막 행이면 새로운 행 추가 + if (index === fields.length - 1) { + handleAddItem(); + } else { + // 아니면 다음 행의 아이템 선택으로 이동 + const button = inputRefs.current[index + 1]; + if (button) { + setTimeout(() => button.click(), 10); + } + } + } + }} + /> + + + + )} + /> + ) : ( +
+ {watchItems[index]?.uom || "each"} +
+ )} + + {/* remove row - 편집 모드에서만 표시 */} + {isEditable && ( + + + + + + +

아이템 삭제

+
+
+
+ )} +
+ ); + })} +
+ +
+
+ {isEditable ? ( + <> + + + + + + +

단축키: Alt+N

+
+
+
+ + {fields.length}개 아이템 + + {deletedItemIds.length > 0 && ( + + ({deletedItemIds.length}개 아이템 삭제 예정) + + )} + + ) : ( + + {fields.length}개 아이템 + + )} +
+ + {isEditable && ( +
+ + Tab + 필드 간 이동 + + + Enter + 다음 필드로 이동 + +
+ )} +
+
+ + + {isEditable ? ( + <> + + + + + + 변경사항을 저장하지 않고 나가기 + + + + + + + + + +

단축키: Ctrl+S

+
+
+
+ + ) : ( + + )} +
+
+ +
+
+
+ + {/* 저장하지 않고 나가기 확인 다이얼로그 - 편집 모드에서만 활성화 */} + {isEditable && ( + + + + 저장되지 않은 변경사항 + + 저장되지 않은 변경사항이 있습니다. 그래도 나가시겠습니까? + + + + 취소 + { + setIsExitDialogOpen(false); + onOpenChange(false); + }}> + 저장하지 않고 나가기 + + + + + )} + + ); +} \ No newline at end of file diff --git a/lib/rfqs/table/add-rfq-dialog.tsx b/lib/rfqs/table/add-rfq-dialog.tsx new file mode 100644 index 00000000..1d824bc0 --- /dev/null +++ b/lib/rfqs/table/add-rfq-dialog.tsx @@ -0,0 +1,349 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Check, ChevronsUpDown } from "lucide-react" +import { toast } from "sonner" + +import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" +import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command" + +import { useSession } from "next-auth/react" +import { createRfqSchema, type CreateRfqSchema, RfqType } from "../validations" +import { createRfq, getBudgetaryRfqs } from "../service" +import { ProjectSelector } from "@/components/ProjectSelector" +import { type Project } from "../service" +import { cn } from "@/lib/utils" +import { BudgetaryRfqSelector } from "./BudgetaryRfqSelector" +import { type BudgetaryRfq as ServiceBudgetaryRfq } from "../service"; + +// 부모 RFQ 정보 타입 정의 +interface BudgetaryRfq { + id: number; + rfqCode: string; + description: string | null; +} + +interface AddRfqDialogProps { + rfqType?: RfqType; +} + +export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps) { + const [open, setOpen] = React.useState(false) + const { data: session, status } = useSession() + const [budgetaryRfqs, setBudgetaryRfqs] = React.useState([]) + const [isLoadingBudgetary, setIsLoadingBudgetary] = React.useState(false) + const [budgetarySearchOpen, setBudgetarySearchOpen] = React.useState(false) + const [budgetarySearchTerm, setBudgetarySearchTerm] = React.useState("") + const [selectedBudgetaryRfq, setSelectedBudgetaryRfq] = React.useState(null) + + // Get the user ID safely, ensuring it's a valid number + const userId = React.useMemo(() => { + const id = session?.user?.id ? Number(session.user.id) : null; + + // Debug logging - remove in production + console.log("Session status:", status); + console.log("Session data:", session); + console.log("User ID:", id); + + return id; + }, [session, status]); + + // RfqType에 따른 타이틀 생성 + const getTitle = () => { + return rfqType === RfqType.PURCHASE + ? "Purchase RFQ" + : "Budgetary RFQ"; + }; + + // RHF + Zod + const form = useForm({ + resolver: zodResolver(createRfqSchema), + defaultValues: { + rfqCode: "", + description: "", + projectId: undefined, + parentRfqId: undefined, + dueDate: new Date(), + status: "DRAFT", + rfqType: rfqType, + // Don't set createdBy yet - we'll set it when the form is submitted + createdBy: undefined, + }, + }); + + // Update form values when session loads + React.useEffect(() => { + if (status === "authenticated" && userId) { + form.setValue("createdBy", userId); + } + }, [status, userId, form]); + + // Budgetary RFQ 목록 로드 (Purchase RFQ 생성 시만) + React.useEffect(() => { + if (rfqType === RfqType.PURCHASE && open) { + const loadBudgetaryRfqs = async () => { + setIsLoadingBudgetary(true); + try { + const result = await getBudgetaryRfqs(); + if ('rfqs' in result) { + setBudgetaryRfqs(result.rfqs as unknown as BudgetaryRfq[]); + } else if ('error' in result) { + console.error("Budgetary RFQs 로드 오류:", result.error); + } + } catch (error) { + console.error("Budgetary RFQs 로드 오류:", error); + } finally { + setIsLoadingBudgetary(false); + } + }; + + loadBudgetaryRfqs(); + } + }, [rfqType, open]); + + // 검색어로 필터링된 Budgetary RFQ 목록 + const filteredBudgetaryRfqs = React.useMemo(() => { + if (!budgetarySearchTerm.trim()) return budgetaryRfqs; + + const lowerSearch = budgetarySearchTerm.toLowerCase(); + return budgetaryRfqs.filter( + rfq => + rfq.rfqCode.toLowerCase().includes(lowerSearch) || + (rfq.description && rfq.description.toLowerCase().includes(lowerSearch)) + ); + }, [budgetaryRfqs, budgetarySearchTerm]); + + // 프로젝트 선택 처리 + const handleProjectSelect = (project: Project) => { + form.setValue("projectId", project.id); + }; + + // Budgetary RFQ 선택 처리 + const handleBudgetaryRfqSelect = (rfq: BudgetaryRfq) => { + setSelectedBudgetaryRfq(rfq); + form.setValue("parentRfqId", rfq.id); + setBudgetarySearchOpen(false); + }; + + async function onSubmit(data: CreateRfqSchema) { + // Check if user is authenticated before submitting + if (status !== "authenticated" || !userId) { + toast.error("사용자 인증이 필요합니다. 다시 로그인해주세요."); + return; + } + + // Make sure createdBy is set with the current user ID + const submitData = { + ...data, + createdBy: userId + }; + + console.log("Submitting form data:", submitData); + + const result = await createRfq(submitData); + if (result.error) { + toast.error(`에러: ${result.error}`); + return; + } + + toast.success("RFQ가 성공적으로 생성되었습니다."); + form.reset(); + setSelectedBudgetaryRfq(null); + setOpen(false); + } + + function handleDialogOpenChange(nextOpen: boolean) { + if (!nextOpen) { + form.reset(); + setSelectedBudgetaryRfq(null); + } + setOpen(nextOpen); + } + + // Return a message or disabled state if user is not authenticated + if (status === "loading") { + return ; + } + + return ( + + {/* 모달을 열기 위한 버튼 */} + + + + + + + Create New {getTitle()} + + 새 {getTitle()} 정보를 입력하고 Create 버튼을 누르세요. + + + +
+ +
+ {/* rfqType - hidden field */} + ( + + )} + /> + + {/* Project Selector */} + ( + + Project + + + + + + )} + /> + + {/* Budgetary RFQ Selector - 구매용 RFQ 생성 시에만 표시 */} + {rfqType === RfqType.PURCHASE && ( + ( + + Budgetary RFQ (Optional) + + { + setSelectedBudgetaryRfq(rfq as any); + form.setValue("parentRfqId", rfq?.id); + }} + placeholder="Budgetary RFQ 선택..." + /> + + + + )} + /> + )} + + {/* rfqCode */} + ( + + RFQ Code + + + + + + )} + /> + + {/* description */} + ( + + RFQ Description + + + + + + )} + /> + + {/* dueDate */} + ( + + Due Date + + { + const val = e.target.value + if (val) { + field.onChange(new Date(val + "T00:00:00")) + } + }} + /> + + + + )} + /> + + {/* status (Read-only) */} + ( + + Status + + {}} // Prevent changes + /> + + + + )} + /> +
+ + + + + +
+ +
+
+ ) +} \ No newline at end of file diff --git a/lib/rfqs/table/attachment-rfq-sheet.tsx b/lib/rfqs/table/attachment-rfq-sheet.tsx new file mode 100644 index 00000000..57a170e1 --- /dev/null +++ b/lib/rfqs/table/attachment-rfq-sheet.tsx @@ -0,0 +1,430 @@ +"use client" + +import * as React from "react" +import { z } from "zod" +import { useForm, useFieldArray } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" + +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetDescription, + SheetFooter, + SheetClose, +} from "@/components/ui/sheet" +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription +} from "@/components/ui/form" +import { Trash2, Plus, Loader, Download, X, Eye, AlertCircle } from "lucide-react" +import { useToast } from "@/hooks/use-toast" +import { Badge } from "@/components/ui/badge" + +import { + Dropzone, + DropzoneDescription, + DropzoneInput, + DropzoneTitle, + DropzoneUploadIcon, + DropzoneZone, +} from "@/components/ui/dropzone" +import { + FileList, + FileListAction, + FileListDescription, + FileListHeader, + FileListIcon, + FileListInfo, + FileListItem, + FileListName, + FileListSize, +} from "@/components/ui/file-list" + +import prettyBytes from "pretty-bytes" +import { processRfqAttachments } from "../service" +import { format } from "path" +import { formatDate } from "@/lib/utils" +import { RfqType } from "../validations" +import { RfqWithItemCount } from "@/db/schema/rfq" + +const MAX_FILE_SIZE = 6e8 // 600MB + +/** 기존 첨부 파일 정보 */ +interface ExistingAttachment { + id: number + fileName: string + filePath: string + createdAt?: Date // or Date + vendorId?: number | null + size?: number +} + +/** 새로 업로드할 파일 */ +const newUploadSchema = z.object({ + fileObj: z.any().optional(), // 실제 File +}) + +/** 기존 첨부 (react-hook-form에서 관리) */ +const existingAttachSchema = z.object({ + id: z.number(), + fileName: z.string(), + filePath: z.string(), + vendorId: z.number().nullable().optional(), + createdAt: z.custom().optional(), // or use z.any().optional() + size: z.number().optional(), +}) + +/** RHF 폼 전체 스키마 */ +const attachmentsFormSchema = z.object({ + rfqId: z.number().int(), + existing: z.array(existingAttachSchema), + newUploads: z.array(newUploadSchema), +}) + +type AttachmentsFormValues = z.infer + +interface RfqAttachmentsSheetProps + extends React.ComponentPropsWithRef { + defaultAttachments?: ExistingAttachment[] + rfqType?: RfqType + rfq: RfqWithItemCount | null + /** 업로드/삭제 후 상위 테이블에 itemCount 등을 업데이트하기 위한 콜백 */ + onAttachmentsUpdated?: (rfqId: number, newItemCount: number) => void +} + +/** + * RfqAttachmentsSheet: + * - 기존 첨부 목록 (다운로드 + 삭제) + * - 새 파일 Dropzone + * - Save 시 processRfqAttachments(server action) + */ +export function RfqAttachmentsSheet({ + defaultAttachments = [], + onAttachmentsUpdated, + rfq, + rfqType, + ...props +}: RfqAttachmentsSheetProps) { + const { toast } = useToast() + const [isPending, startUpdate] = React.useTransition() + const rfqId = rfq?.rfqId ?? 0; + + // 편집 가능 여부 확인 - DRAFT 상태일 때만 편집 가능 + const isEditable = rfq?.status === "DRAFT"; + + // React Hook Form + const form = useForm({ + resolver: zodResolver(attachmentsFormSchema), + defaultValues: { + rfqId, + existing: [], + newUploads: [], + }, + }) + + const { reset, control, handleSubmit } = form + + // defaultAttachments가 바뀔 때마다, RHF 상태를 reset + React.useEffect(() => { + reset({ + rfqId, + existing: defaultAttachments.map((att) => ({ + ...att, + vendorId: att.vendorId ?? null, + size: att.size ?? undefined, + })), + newUploads: [], + }) + }, [rfqId, defaultAttachments, reset]) + + // Field Arrays + const { + fields: existingFields, + remove: removeExisting, + } = useFieldArray({ control, name: "existing" }) + + const { + fields: newUploadFields, + append: appendNewUpload, + remove: removeNewUpload, + } = useFieldArray({ control, name: "newUploads" }) + + // 기존 첨부 항목 중 삭제된 것 찾기 + function findRemovedExistingIds(data: AttachmentsFormValues): number[] { + const finalIds = data.existing.map((att) => att.id) + const originalIds = defaultAttachments.map((att) => att.id) + return originalIds.filter((id) => !finalIds.includes(id)) + } + + async function onSubmit(data: AttachmentsFormValues) { + // 편집 불가능한 상태에서는 제출 방지 + if (!isEditable) return; + + startUpdate(async () => { + try { + const removedExistingIds = findRemovedExistingIds(data) + const newFiles = data.newUploads + .map((it) => it.fileObj) + .filter((f): f is File => !!f) + + // 서버 액션 + const res = await processRfqAttachments({ + rfqId, + removedExistingIds, + newFiles, + vendorId: null, // vendor ID if needed + rfqType + }) + + if (!res.ok) throw new Error(res.error ?? "Unknown error") + + const newCount = res.updatedItemCount ?? 0 + + toast({ + variant: "default", + title: "Success", + description: "File(s) updated", + }) + + // 상위 테이블 등에 itemCount 업데이트 + onAttachmentsUpdated?.(rfqId, newCount) + + // 모달 닫기 + props.onOpenChange?.(false) + } catch (err) { + toast({ + variant: "destructive", + title: "Error", + description: String(err), + }) + } + }) + } + + /** 기존 첨부 - X 버튼 */ + function handleRemoveExisting(idx: number) { + // 편집 불가능한 상태에서는 삭제 방지 + if (!isEditable) return; + removeExisting(idx) + } + + /** 드롭존에서 파일 받기 */ + function handleDropAccepted(acceptedFiles: File[]) { + // 편집 불가능한 상태에서는 파일 추가 방지 + if (!isEditable) return; + const mapped = acceptedFiles.map((file) => ({ fileObj: file })) + appendNewUpload(mapped) + } + + /** 드롭존에서 파일 거부(에러) */ + function handleDropRejected(fileRejections: any[]) { + // 편집 불가능한 상태에서는 무시 + if (!isEditable) return; + + fileRejections.forEach((rej) => { + toast({ + variant: "destructive", + title: "File Error", + description: rej.file.name + " not accepted", + }) + }) + } + + return ( + + + + + {isEditable ? "Manage Attachments" : "View Attachments"} + {rfq?.status && ( + + {rfq.status} + + )} + + + {`RFQ ${rfq?.rfqCode} - `} + {isEditable ? '파일 첨부/삭제' : '첨부 파일 보기'} + {!isEditable && ( +
+ + 드래프트 상태가 아닌 RFQ는 첨부파일을 수정할 수 없습니다. +
+ )} +
+
+ +
+ + {/* 1) 기존 첨부 목록 */} +
+

Existing Attachments

+ {existingFields.length === 0 && ( +

No existing attachments

+ )} + {existingFields.map((field, index) => { + const vendorLabel = field.vendorId ? "(Vendor)" : "(Internal)" + return ( +
+
+ + {field.fileName} {vendorLabel} + + {field.size && ( + + {Math.round(field.size / 1024)} KB + + )} + {field.createdAt && ( + + Created at {formatDate(field.createdAt)} + + )} +
+
+ {/* 1) Download button (if filePath) */} + {field.filePath && ( + + + + )} + {/* 2) Remove button - 편집 가능할 때만 표시 */} + {isEditable && ( + + )} +
+
+ ) + })} +
+ + {/* 2) Dropzone for new uploads - 편집 가능할 때만 표시 */} + {isEditable ? ( + <> + + {({ maxSize }) => ( + ( + + Drop Files Here + + + + +
+ +
+ Drop to upload + + Max size: {maxSize ? prettyBytes(maxSize) : "??? MB"} + +
+
+
+ Alternatively, click browse. + +
+ )} + /> + )} +
+ + {/* newUpload fields -> FileList */} + {newUploadFields.length > 0 && ( +
+
+ {`Files (${newUploadFields.length})`} +
+ + {newUploadFields.map((field, idx) => { + const fileObj = form.getValues(`newUploads.${idx}.fileObj`) + if (!fileObj) return null + + const fileName = fileObj.name + const fileSize = fileObj.size + return ( + + + + + {fileName} + + {`${prettyBytes(fileSize)}`} + + + removeNewUpload(idx)}> + + Remove + + + + ) + })} + +
+ )} + + ) : ( +
+
+ +

보기 모드에서는 파일 첨부를 할 수 없습니다.

+
+
+ )} + + + + + + {isEditable && ( + + )} + +
+ +
+
+ ) +} \ No newline at end of file diff --git a/lib/rfqs/table/delete-rfqs-dialog.tsx b/lib/rfqs/table/delete-rfqs-dialog.tsx new file mode 100644 index 00000000..09596bc7 --- /dev/null +++ b/lib/rfqs/table/delete-rfqs-dialog.tsx @@ -0,0 +1,149 @@ +"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 { Rfq, RfqWithItemCount } from "@/db/schema/rfq" +import { removeRfqs } from "../service" + +interface DeleteRfqsDialogProps + extends React.ComponentPropsWithoutRef { + rfqs: Row["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeleteRfqsDialog({ + rfqs, + showTrigger = true, + onSuccess, + ...props +}: DeleteRfqsDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onDelete() { + startDeleteTransition(async () => { + const { error } = await removeRfqs({ + ids: rfqs.map((rfq) => rfq.rfqId), + }) + + if (error) { + toast.error(error) + return + } + + props.onOpenChange?.(false) + toast.success("Tasks deleted") + onSuccess?.() + }) + } + + if (isDesktop) { + return ( + + {showTrigger ? ( + + + + ) : null} + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your{" "} + {rfqs.length} + {rfqs.length === 1 ? " task" : " rfqs"} from our servers. + + + + + + + + + + + ) + } + + return ( + + {showTrigger ? ( + + + + ) : null} + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your{" "} + {rfqs.length} + {rfqs.length === 1 ? " task" : " rfqs"} from our servers. + + + + + + + + + + + ) +} diff --git a/lib/rfqs/table/feature-flags-provider.tsx b/lib/rfqs/table/feature-flags-provider.tsx new file mode 100644 index 00000000..81131894 --- /dev/null +++ b/lib/rfqs/table/feature-flags-provider.tsx @@ -0,0 +1,108 @@ +"use client" + +import * as React from "react" +import { useQueryState } from "nuqs" + +import { dataTableConfig, type DataTableConfig } from "@/config/data-table" +import { cn } from "@/lib/utils" +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" + +type FeatureFlagValue = DataTableConfig["featureFlags"][number]["value"] + +interface FeatureFlagsContextProps { + featureFlags: FeatureFlagValue[] + setFeatureFlags: (value: FeatureFlagValue[]) => void +} + +const FeatureFlagsContext = React.createContext({ + featureFlags: [], + setFeatureFlags: () => {}, +}) + +export function useFeatureFlags() { + const context = React.useContext(FeatureFlagsContext) + if (!context) { + throw new Error( + "useFeatureFlags must be used within a FeatureFlagsProvider" + ) + } + return context +} + +interface FeatureFlagsProviderProps { + children: React.ReactNode +} + +export function FeatureFlagsProvider({ children }: FeatureFlagsProviderProps) { + const [featureFlags, setFeatureFlags] = useQueryState( + "flags", + { + defaultValue: [], + parse: (value) => value.split(",") as FeatureFlagValue[], + serialize: (value) => value.join(","), + eq: (a, b) => + a.length === b.length && a.every((value, index) => value === b[index]), + clearOnDefault: true, + shallow: false, + } + ) + + return ( + void setFeatureFlags(value), + }} + > +
+ setFeatureFlags(value)} + className="w-fit gap-0" + > + {dataTableConfig.featureFlags.map((flag, index) => ( + + + + + + +
{flag.tooltipTitle}
+
+ {flag.tooltipDescription} +
+
+
+ ))} +
+
+ {children} +
+ ) +} diff --git a/lib/rfqs/table/feature-flags.tsx b/lib/rfqs/table/feature-flags.tsx new file mode 100644 index 00000000..aaae6af2 --- /dev/null +++ b/lib/rfqs/table/feature-flags.tsx @@ -0,0 +1,96 @@ +"use client" + +import * as React from "react" +import { useQueryState } from "nuqs" + +import { dataTableConfig, type DataTableConfig } from "@/config/data-table" +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" + +type FeatureFlagValue = DataTableConfig["featureFlags"][number]["value"] + +interface TasksTableContextProps { + featureFlags: FeatureFlagValue[] + setFeatureFlags: (value: FeatureFlagValue[]) => void +} + +const TasksTableContext = React.createContext({ + featureFlags: [], + setFeatureFlags: () => {}, +}) + +export function useTasksTable() { + const context = React.useContext(TasksTableContext) + if (!context) { + throw new Error("useTasksTable must be used within a TasksTableProvider") + } + return context +} + +export function TasksTableProvider({ children }: React.PropsWithChildren) { + const [featureFlags, setFeatureFlags] = useQueryState( + "featureFlags", + { + defaultValue: [], + parse: (value) => value.split(",") as FeatureFlagValue[], + serialize: (value) => value.join(","), + eq: (a, b) => + a.length === b.length && a.every((value, index) => value === b[index]), + clearOnDefault: true, + } + ) + + return ( + void setFeatureFlags(value), + }} + > +
+ setFeatureFlags(value)} + className="w-fit" + > + {dataTableConfig.featureFlags.map((flag) => ( + + + + + + +
{flag.tooltipTitle}
+
+ {flag.tooltipDescription} +
+
+
+ ))} +
+
+ {children} +
+ ) +} diff --git a/lib/rfqs/table/rfqs-table-columns.tsx b/lib/rfqs/table/rfqs-table-columns.tsx new file mode 100644 index 00000000..98df3bc8 --- /dev/null +++ b/lib/rfqs/table/rfqs-table-columns.tsx @@ -0,0 +1,315 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Ellipsis, Paperclip, Package } from "lucide-react" +import { toast } from "sonner" + +import { formatDate } 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, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +import { getRFQStatusIcon } from "@/lib/tasks/utils" +import { rfqsColumnsConfig } from "@/config/rfqsColumnsConfig" +import { RfqWithItemCount } from "@/db/schema/rfq" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { useRouter } from "next/navigation" +import { RfqType } from "../validations" + +type NextRouter = ReturnType; + +interface GetColumnsProps { + setRowAction: React.Dispatch< + React.SetStateAction | null> + > + openItemsModal: (rfqId: number) => void + openAttachmentsSheet: (rfqId: number) => void + router: NextRouter + rfqType?: RfqType +} + +/** + * tanstack table 컬럼 정의 (중첩 헤더 버전) + */ +export function getColumns({ + setRowAction, + openItemsModal, + openAttachmentsSheet, + router, + rfqType, +}: GetColumnsProps): ColumnDef[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef = { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + } + + // ---------------------------------------------------------------- + // 2) actions 컬럼 (Dropdown 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + + // Proceed 버튼 클릭 시 호출되는 함수 + const handleProceed = () => { + const rfq = row.original + const itemCount = Number(rfq.itemCount || 0) + const attachCount = Number(rfq.attachCount || 0) + + // 아이템과 첨부파일이 모두 0보다 커야 진행 가능 + if (itemCount > 0 && attachCount > 0) { + router.push( + rfqType === RfqType.PURCHASE + ? `/evcp/rfq/${rfq.rfqId}` + : `/evcp/budgetary/${rfq.rfqId}` + ) + } else { + // 조건을 충족하지 않는 경우 토스트 알림 표시 + if (itemCount === 0 && attachCount === 0) { + toast.error("아이템과 첨부파일을 먼저 추가해주세요.") + } else if (itemCount === 0) { + toast.error("아이템을 먼저 추가해주세요.") + } else { + toast.error("첨부파일을 먼저 추가해주세요.") + } + } + } + + return ( + + + + + + setRowAction({ row, type: "update" })} + > + Edit + + + + {row.original.status ==="DRAFT"?"Proceed":"View Detail"} + + + + setRowAction({ row, type: "delete" })} + > + Delete + ⌘⌫ + + + + ) + }, + size: 40, + } + + // ---------------------------------------------------------------- + // 3) itemsColumn (아이템 개수 표시: 아이콘 + Badge) + // ---------------------------------------------------------------- + const itemsColumn: ColumnDef = { + id: "items", + header: "Items", + cell: ({ row }) => { + const rfq = row.original + const itemCount = rfq.itemCount || 0 + + const handleClick = () => { + openItemsModal(rfq.rfqId) + } + + return ( + + ) + }, + enableSorting: false, + size: 60, + } + + // ---------------------------------------------------------------- + // 4) attachmentsColumn (첨부파일 개수 표시: 아이콘 + Badge) + // ---------------------------------------------------------------- + const attachmentsColumn: ColumnDef = { + id: "attachments", + header: "Attachments", + cell: ({ row }) => { + const fileCount = row.original.attachCount ?? 0 + + const handleClick = () => { + openAttachmentsSheet(row.original.rfqId) + } + + return ( + + ) + }, + enableSorting: false, + size: 60, + } + + // ---------------------------------------------------------------- + // 5) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 + // ---------------------------------------------------------------- + const groupMap: Record[]> = {} + + rfqsColumnsConfig.forEach((cfg) => { + const groupName = cfg.group || "_noGroup" + + if (!groupMap[groupName]) { + groupMap[groupName] = [] + } + + // child column 정의 + const childCol: ColumnDef = { + accessorKey: cfg.id, + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: cfg.excelHeader, + group: cfg.group, + type: cfg.type, + }, + cell: ({ row, cell }) => { + if (cfg.id === "status") { + const statusVal = row.original.status + if (!statusVal) return null + const Icon = getRFQStatusIcon( + statusVal as "DRAFT" | "PUBLISHED" | "EVALUATION" | "AWARDED" + ) + return ( +
+
+ ) + } + + if (cfg.id === "createdAt" || cfg.id === "updatedAt") { + const dateVal = cell.getValue() as Date + return formatDate(dateVal) + } + + return row.getValue(cfg.id) ?? "" + }, + } + + groupMap[groupName].push(childCol) + }) + + // groupMap -> nestedColumns + const nestedColumns: ColumnDef[] = [] + Object.entries(groupMap).forEach(([groupName, colDefs]) => { + if (groupName === "_noGroup") { + nestedColumns.push(...colDefs) + } else { + nestedColumns.push({ + id: groupName, + header: groupName, + columns: colDefs, + }) + } + }) + + // ---------------------------------------------------------------- + // 6) 최종 컬럼 배열 + // ---------------------------------------------------------------- + return [ + selectColumn, + ...nestedColumns, + attachmentsColumn, // 첨부파일 + actionsColumn, + itemsColumn, // 아이템 + ] +} \ No newline at end of file diff --git a/lib/rfqs/table/rfqs-table-floating-bar.tsx b/lib/rfqs/table/rfqs-table-floating-bar.tsx new file mode 100644 index 00000000..daef7e0b --- /dev/null +++ b/lib/rfqs/table/rfqs-table-floating-bar.tsx @@ -0,0 +1,338 @@ +"use client" + +import * as React from "react" +import { Table } from "@tanstack/react-table" +import { toast } from "sonner" +import { Calendar, type CalendarProps } from "@/components/ui/calendar" +import { Button } from "@/components/ui/button" +import { Portal } from "@/components/ui/portal" +import { + Select, + SelectTrigger, + SelectContent, + SelectGroup, + SelectItem, + SelectValue, +} from "@/components/ui/select" +import { Separator } from "@/components/ui/separator" +import { + Tooltip, + TooltipTrigger, + TooltipContent, +} from "@/components/ui/tooltip" +import { Kbd } from "@/components/kbd" +import { ActionConfirmDialog } from "@/components/ui/action-dialog" + +import { ArrowUp, CheckCircle2, Download, Loader, Trash2, X, CalendarIcon } from "lucide-react" + +import { exportTableToExcel } from "@/lib/export" + +import { RfqWithItemCount, rfqs } from "@/db/schema/rfq" +import { modifyRfqs, removeRfqs } from "../service" + +interface RfqsTableFloatingBarProps { + table: Table +} + +/** + * 추가된 로직: + * - 달력(캘린더) 아이콘 버튼 + * - 눌렀을 때 Popover로 Calendar 표시 + * - 날짜 선택 시 Confirm 다이얼로그 → modifyRfqs({ dueDate }) + */ +export function RfqsTableFloatingBar({ table }: RfqsTableFloatingBarProps) { + const rows = table.getFilteredSelectedRowModel().rows + const [isPending, startTransition] = React.useTransition() + const [action, setAction] = React.useState<"update-status" | "export" | "delete" | "update-dueDate">() + const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false) + + const [confirmProps, setConfirmProps] = React.useState<{ + title: string + description?: string + onConfirm: () => Promise | void + }>({ + title: "", + description: "", + onConfirm: () => {}, + }) + + // 캘린더 Popover 열림 여부 + const [calendarOpen, setCalendarOpen] = React.useState(false) + const [selectedDate, setSelectedDate] = React.useState(null) + + // Clear selection on Escape key press + React.useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") { + table.toggleAllRowsSelected(false) + } + } + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [table]) + + function handleDeleteConfirm() { + setAction("delete") + setConfirmProps({ + title: `Delete ${rows.length} RFQ${rows.length > 1 ? "s" : ""}?`, + description: "This action cannot be undone.", + onConfirm: async () => { + startTransition(async () => { + const { error } = await removeRfqs({ + ids: rows.map((row) => row.original.rfqId), + }) + if (error) { + toast.error(error) + return + } + toast.success("RFQs deleted") + table.toggleAllRowsSelected(false) + setConfirmDialogOpen(false) + }) + }, + }) + setConfirmDialogOpen(true) + } + + function handleSelectStatus(newStatus: RfqWithItemCount["status"]) { + setAction("update-status") + setConfirmProps({ + title: `Update ${rows.length} RFQ${rows.length > 1 ? "s" : ""} with status: ${newStatus}?`, + description: "This action will override their current status.", + onConfirm: async () => { + startTransition(async () => { + const { error } = await modifyRfqs({ + ids: rows.map((row) => row.original.rfqId), + status: newStatus as "DRAFT" | "PUBLISHED" | "EVALUATION" | "AWARDED", + }) + if (error) { + toast.error(error) + return + } + toast.success("RFQs updated") + setConfirmDialogOpen(false) + }) + }, + }) + setConfirmDialogOpen(true) + } + + // 1) 달력에서 날짜를 선택했을 때 → Confirm 다이얼로그 + function handleDueDateSelect(newDate: Date) { + setAction("update-dueDate") + + setConfirmProps({ + title: `Update ${rows.length} RFQ${rows.length > 1 ? "s" : ""} Due Date to ${newDate.toDateString()}?`, + description: "This action will override their current due date.", + onConfirm: async () => { + startTransition(async () => { + const { error } = await modifyRfqs({ + ids: rows.map((r) => r.original.rfqId), + dueDate: newDate, + }) + if (error) { + toast.error(error) + return + } + toast.success("Due date updated") + setConfirmDialogOpen(false) + setCalendarOpen(false) + }) + }, + }) + setConfirmDialogOpen(true) + } + + // 2) Export + function handleExport() { + setAction("export") + startTransition(() => { + exportTableToExcel(table, { + excludeColumns: ["select", "actions"], + onlySelected: true, + }) + }) + } + + // Floating bar UI + return ( + +
+
+
+ {/* Selection Info + Clear */} +
+ + {rows.length} selected + + + + + + + +

Clear selection

+ + Esc + +
+
+
+ + + +
+ {/* 1) Status Update */} + + + {/* 2) Due Date Update: Calendar Popover */} + + + + + +

Update Due Date

+
+
+ + {/* Calendar Popover (간단 구현) */} + {calendarOpen && ( +
+ { + if (date) { + setSelectedDate(date) + handleDueDateSelect(date) + } + }} + initialFocus + /> +
+ )} + + {/* 3) Export */} + + + + + +

Export tasks

+
+
+ + {/* 4) Delete */} + + + + + +

Delete tasks

+
+
+
+
+
+
+ + {/* 공용 Confirm Dialog */} + +
+ ) +} \ No newline at end of file diff --git a/lib/rfqs/table/rfqs-table-toolbar-actions.tsx b/lib/rfqs/table/rfqs-table-toolbar-actions.tsx new file mode 100644 index 00000000..6402e625 --- /dev/null +++ b/lib/rfqs/table/rfqs-table-toolbar-actions.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" +import { Download, Upload } from "lucide-react" +import { toast } from "sonner" + +import { exportTableToExcel } from "@/lib/export" +import { Button } from "@/components/ui/button" +import { RfqWithItemCount } from "@/db/schema/rfq" +import { DeleteRfqsDialog } from "./delete-rfqs-dialog" +import { AddRfqDialog } from "./add-rfq-dialog" +import { RfqType } from "../validations" + + +interface RfqsTableToolbarActionsProps { + table: Table + rfqType?: RfqType; +} + +export function RfqsTableToolbarActions({ table , rfqType = RfqType.PURCHASE}: RfqsTableToolbarActionsProps) { + return ( +
+ {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */} + {table.getFilteredSelectedRowModel().rows.length > 0 ? ( + row.original)} + onSuccess={() => table.toggleAllRowsSelected(false)} + /> + ) : null} + + {/** 2) 새 Task 추가 다이얼로그 */} + + + + {/** 4) Export 버튼 */} + +
+ ) +} \ No newline at end of file diff --git a/lib/rfqs/table/rfqs-table.tsx b/lib/rfqs/table/rfqs-table.tsx new file mode 100644 index 00000000..7d996816 --- /dev/null +++ b/lib/rfqs/table/rfqs-table.tsx @@ -0,0 +1,264 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { toSentenceCase } from "@/lib/utils" +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 { getRFQStatusIcon } from "@/lib/tasks/utils" +import { useFeatureFlags } from "./feature-flags-provider" +import { getColumns } from "./rfqs-table-columns" +import { fetchRfqAttachments, fetchRfqItems, getRfqs, getRfqStatusCounts } from "../service" +import { RfqItem, RfqWithItemCount, rfqs } from "@/db/schema/rfq" +import { RfqsTableFloatingBar } from "./rfqs-table-floating-bar" +import { UpdateRfqSheet } from "./update-rfq-sheet" +import { DeleteRfqsDialog } from "./delete-rfqs-dialog" +import { RfqsTableToolbarActions } from "./rfqs-table-toolbar-actions" +import { RfqsItemsDialog } from "./ItemsDialog" +import { getAllItems } from "@/lib/items/service" +import { RfqAttachmentsSheet } from "./attachment-rfq-sheet" +import { useRouter } from "next/navigation" +import { RfqType } from "../validations" + +interface RfqsTableProps { + promises: Promise< + [ + Awaited>, + Awaited>, + Awaited>, + ] + >; + rfqType?: RfqType; // rfqType props 추가 +} + +export interface ExistingAttachment { + id: number; + fileName: string; + filePath: string; + createdAt?: Date; + vendorId?: number | null; + size?: number; +} + +export interface ExistingItem { + id?: number; + itemCode: string; + description: string | null; + quantity: number | null; + uom: string | null; +} + +export function RfqsTable({ promises, rfqType = RfqType.PURCHASE }: RfqsTableProps) { + const { featureFlags } = useFeatureFlags() + + const [{ data, pageCount }, statusCounts, items] = React.use(promises) + const [attachmentsOpen, setAttachmentsOpen] = React.useState(false) + const [selectedRfqIdForAttachments, setSelectedRfqIdForAttachments] = React.useState(null) + const [attachDefault, setAttachDefault] = React.useState([]) + const [itemsDefault, setItemsDefault] = React.useState([]) + + const router = useRouter() + + const itemsList = items?.map((v) => ({ + code: v.itemCode ?? "", + name: v.itemName ?? "", + })); + + const [rowAction, setRowAction] = + React.useState | null>(null) + + const [rowData, setRowData] = React.useState(() => data) + + const [itemsModalOpen, setItemsModalOpen] = React.useState(false); + const [selectedRfqId, setSelectedRfqId] = React.useState(null); + + + const selectedRfq = React.useMemo(() => { + return rowData.find(row => row.rfqId === selectedRfqId) || null; + }, [rowData, selectedRfqId]); + + // rfqType에 따른 제목 계산 + const getRfqTypeTitle = () => { + return rfqType === RfqType.PURCHASE ? "Purchase RFQ" : "Budgetary RFQ"; + }; + + async function openItemsModal(rfqId: number) { + const itemList = await fetchRfqItems(rfqId) + setItemsDefault(itemList) + setSelectedRfqId(rfqId); + setItemsModalOpen(true); + } + + async function openAttachmentsSheet(rfqId: number) { + // 4.1) Fetch current attachments from server (server action) + const list = await fetchRfqAttachments(rfqId) // returns ExistingAttachment[] + setAttachDefault(list) + setSelectedRfqIdForAttachments(rfqId) + setAttachmentsOpen(true) + setSelectedRfqId(rfqId); + } + + function handleAttachmentsUpdated(rfqId: number, newCount: number, newList?: ExistingAttachment[]) { + // 5.1) update rowData itemCount + setRowData(prev => + prev.map(r => + r.rfqId === rfqId + ? { ...r, itemCount: newCount } + : r + ) + ) + // 5.2) if newList is provided, store it + if (newList) { + setAttachDefault(newList) + } + } + + const columns = React.useMemo(() => getColumns({ + setRowAction, router, + // we pass openItemsModal as a prop so the itemsColumn can call it + openItemsModal, + openAttachmentsSheet, + rfqType + }), [setRowAction, router, rfqType]); + + /** + * This component can render either a faceted filter or a search filter based on the `options` prop. + */ + const filterFields: DataTableFilterField[] = [ + { + id: "rfqCode", + label: "RFQ Code", + placeholder: "Filter RFQ Code...", + }, + { + id: "status", + label: "Status", + options: rfqs.status.enumValues?.map((status) => { + // 명시적으로 status를 허용된 리터럴 타입으로 변환 + const s = status as "DRAFT" | "PUBLISHED" | "EVALUATION" | "AWARDED"; + return { + label: toSentenceCase(s), + value: s, + icon: getRFQStatusIcon(s), + count: statusCounts[s], + }; + }), + + } + ] + + /** + * Advanced filter fields for the data table. + */ + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { + id: "rfqCode", + label: "RFQ Code", + type: "text", + }, + { + id: "description", + label: "Description", + type: "text", + }, + { + id: "projectCode", + label: "Project Code", + type: "text", + }, + { + id: "dueDate", + label: "Due Date", + type: "date", + }, + { + id: "status", + label: "Status", + type: "multi-select", + options: rfqs.status.enumValues?.map((status) => { + // 명시적으로 status를 허용된 리터럴 타입으로 변환 + const s = status as "DRAFT" | "PUBLISHED" | "EVALUATION" | "AWARDED"; + return { + label: toSentenceCase(s), + value: s, + icon: getRFQStatusIcon(s), + count: statusCounts[s], + }; + }), + + }, + ] + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.rfqId), + shallow: false, + clearOnDefault: true, + }) + + return ( +
+ } + > + + + + + + setRowAction(null)} + rfq={rowAction?.row.original ?? null} + rfqType={rfqType} + /> + + setRowAction(null)} + rfqs={rowAction?.row.original ? [rowAction?.row.original] : []} + showTrigger={false} + onSuccess={() => rowAction?.row.toggleSelected(false)} + /> + + + + +
+ ) +} \ No newline at end of file diff --git a/lib/rfqs/table/update-rfq-sheet.tsx b/lib/rfqs/table/update-rfq-sheet.tsx new file mode 100644 index 00000000..769f25e7 --- /dev/null +++ b/lib/rfqs/table/update-rfq-sheet.tsx @@ -0,0 +1,283 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { Loader } from "lucide-react" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import { useSession } from "next-auth/react" + +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Input } from "@/components/ui/input" + +import { Rfq, RfqWithItemCount } from "@/db/schema/rfq" +import { RfqType, updateRfqSchema, type UpdateRfqSchema } from "../validations" +import { modifyRfq } from "../service" +import { ProjectSelector } from "@/components/ProjectSelector" +import { type Project } from "../service" +import { BudgetaryRfqSelector } from "./BudgetaryRfqSelector" + +interface UpdateRfqSheetProps + extends React.ComponentPropsWithRef { + rfq: RfqWithItemCount | null + rfqType?: RfqType; +} + + +interface BudgetaryRfq { + id: number; + rfqCode: string; + description: string | null; +} + + +export function UpdateRfqSheet({ rfq,rfqType = RfqType.PURCHASE , ...props }: UpdateRfqSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + const { data: session } = useSession() + const userId = Number(session?.user?.id || 1) + const [selectedBudgetaryRfq, setSelectedBudgetaryRfq] = React.useState(null) + + // RHF setup + const form = useForm({ + resolver: zodResolver(updateRfqSchema), + defaultValues: { + id: rfq?.rfqId ?? 0, // PK + rfqCode: rfq?.rfqCode ?? "", + description: rfq?.description ?? "", + projectId: rfq?.projectId, // 프로젝트 ID + dueDate: rfq?.dueDate ?? undefined, // null을 undefined로 변환 + status: rfq?.status ?? "DRAFT", + createdBy: rfq?.createdBy ?? userId, + }, + }); + + // 프로젝트 선택 처리 + const handleProjectSelect = (project: Project) => { + form.setValue("projectId", project.id); + }; + + async function onSubmit(input: UpdateRfqSchema) { + startUpdateTransition(async () => { + if (!rfq) return + + const { error } = await modifyRfq({ + ...input, + }) + + if (error) { + toast.error(error) + return + } + + form.reset() + props.onOpenChange?.(false) // close the sheet + toast.success("RFQ updated!") + }) + } + + return ( + + + + Update RFQ + + Update the RFQ details and save the changes + + + + {/* RHF Form */} +
+ + + {/* Hidden or code-based id field */} + ( + + )} + /> + + {/* Project Selector - 재사용 컴포넌트 사용 */} + ( + + Project + + + + + + )} + /> + + {/* Budgetary RFQ Selector - 구매용 RFQ 생성 시에만 표시 */} + {rfqType === RfqType.PURCHASE && ( + ( + + Budgetary RFQ (Optional) + + { + setSelectedBudgetaryRfq(rfq as any); + form.setValue("parentRfqId", rfq?.id); + }} + placeholder="Budgetary RFQ 선택..." + /> + + + + )} + /> + )} + + + {/* rfqCode */} + ( + + RFQ Code + + + + + + )} + /> + + {/* description */} + ( + + Description + + + + + + )} + /> + + + + {/* dueDate (type="date") */} + ( + + Due Date + + yyyy-mm-dd + value={field.value ? field.value.toISOString().slice(0, 10) : ""} + onChange={(e) => { + const val = e.target.value + field.onChange(val ? new Date(val + "T00:00:00") : undefined) + }} + /> + + + + )} + /> + + {/* status (Select) */} + ( + + Status + + + + + + )} + /> + + {/* createdBy (hidden or read-only) */} + ( + + )} + /> + + + + + + + + + +
+
+ ) +} \ No newline at end of file -- cgit v1.2.3 From e6e98a1bed7a23d148ab97b3a7414ade4b1d236b Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 27 Mar 2025 04:30:31 +0000 Subject: 20240327 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../vendor-data/form/[packageId]/[formId]/page.tsx | 40 +- components/form-data/form-data-report-dialog.tsx | 457 ++ .../form-data-report-temp-upload-dialog.tsx | 305 ++ components/form-data/form-data-table-columns.tsx | 77 +- config/poColumnsConfig.ts | 8 + db/migrations/0103_huge_wallflower.sql | 37 + db/migrations/meta/0103_snapshot.json | 4757 ++++++++++++++++++++ db/migrations/meta/_journal.json | 7 + db/schema/contract.ts | 49 +- lib/po/table/po-table-columns.tsx | 95 +- lib/rfqs/table/rfqs-table.tsx | 2 +- lib/vendors/table/request-vendor-pg-dialog.tsx | 4 +- 13 files changed, 5764 insertions(+), 75 deletions(-) create mode 100644 components/form-data/form-data-report-dialog.tsx create mode 100644 components/form-data/form-data-report-temp-upload-dialog.tsx create mode 100644 db/migrations/0103_huge_wallflower.sql create mode 100644 db/migrations/meta/0103_snapshot.json (limited to 'lib/rfqs/table') diff --git a/.gitignore b/.gitignore index 4a325b8c..9b14c704 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ next-env.d.ts /public/uploads /public/vendors /public/profiles +/public/vendorFormData /tmp # 직접 참조가 불가능해 복사가 필요했던 라이브러리 (pdftrone) diff --git a/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/page.tsx b/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/page.tsx index 248bd7fc..58337016 100644 --- a/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/page.tsx +++ b/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/page.tsx @@ -1,30 +1,37 @@ -import DynamicTable from "@/components/form-data/form-data-table" -import { getFormData } from "@/lib/forms/services" +import DynamicTable from "@/components/form-data/form-data-table"; +// 김준회프로입니다. 현재 이 부분에서 컴파일 에러가 발생하여 잠시 주석처리 하겠습니다. +// import { getFormData, getFormId } from "@/lib/forms/services"; +import { getFormData } from "@/lib/forms/services"; interface IndexPageProps { params: { - lng: string - packageId: string - formId: string - } + lng: string; + packageId: string; + formId: string; + }; } export default async function FormPage({ params }: IndexPageProps) { // 1) 구조 분해 할당 - const resolvedParams = await params - + const resolvedParams = await params; + // 2) 구조 분해 할당 - const { lng, packageId, formId } = resolvedParams + const { lng, packageId, formId: formCode } = resolvedParams; // 2) 변환 - const packageIdAsNumber = Number(packageId) + const packageIdAsNumber = Number(packageId); // 3) DB 조회 - const { columns, data } = await getFormData(formId, packageIdAsNumber) + const { columns, data } = await getFormData(formCode, packageIdAsNumber); - // 4) 예외 처리 + // 4) formId 및 report temp file 조회 + const { formId } = await getFormId(packageId, formCode); + + // 5) 예외 처리 if (!columns) { - return

해당 폼의 메타 정보를 불러올 수 없습니다.

+ return ( +

해당 폼의 메타 정보를 불러올 수 없습니다.

+ ); } // 5) 렌더링 @@ -32,10 +39,11 @@ export default async function FormPage({ params }: IndexPageProps) {
- ) -} \ No newline at end of file + ); +} diff --git a/components/form-data/form-data-report-dialog.tsx b/components/form-data/form-data-report-dialog.tsx new file mode 100644 index 00000000..5ddc5e0c --- /dev/null +++ b/components/form-data/form-data-report-dialog.tsx @@ -0,0 +1,457 @@ +"use client"; + +import React, { + FC, + Dispatch, + SetStateAction, + useState, + useEffect, + useRef, +} from "react"; +import { WebViewerInstance, Core } from "@pdftron/webviewer"; +import { useToast } from "@/hooks/use-toast"; +import prettyBytes from "pretty-bytes"; +import { X, Loader2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { getReportTempList } from "@/lib/forms/services"; +import { DataTableColumnJSON } from "./form-data-table-columns"; + +type ReportData = { + [key: string]: any; +}; + +interface tempFile { + fileName: string; + filePath: string; +} + +interface FormDataReportDialogProps { + columnsJSON: DataTableColumnJSON[]; + reportData: ReportData[]; + setReportData: Dispatch>; + packageId: number; + formId: number; + formCode: string; +} + +export const FormDataReportDialog: FC = ({ + columnsJSON, + reportData, + setReportData, + packageId, + formId, +}) => { + const [tempList, setTempList] = useState([]); + const [selectTemp, setSelectTemp] = useState(""); + const [instance, setInstance] = useState(null); + const [fileLoading, setFileLoading] = useState(true); + + useEffect(() => { + updateReportTempList(packageId, formId, setTempList); + }, [packageId, formId]); + + const onClose = async (value: boolean) => { + if (fileLoading) { + return; + } + if (!value) { + setTimeout(() => cleanupHtmlStyle(), 1000); + setReportData([]); + } + }; + + const downloadFileData = async () => { + if (instance) { + const { UI, Core } = instance; + const { documentViewer } = Core; + + const doc = documentViewer.getDocument(); + const fileName = doc.getFilename(); + const fileData = await doc.getFileData({ + includeAnnotations: true, // 사용자가 추가한 폼 필드 및 입력 포함 + // officeOptions: { + // outputFormat: "docx", + // }, + }); + + const blob = new Blob([fileData], { + type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + }); + + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // const allTabs = UI.TabManager.getAllTabs() as { + // id: number; + // src: Core.Document; + // }[]; + + // for (const tab of allTabs) { + // // await UI.TabManager.setActiveTab(tab.id); + // await activateTabAndWaitForLoad(instance, tab.id); + // const tabDoc = tab.src; + // const fileName = tabDoc.getFilename(); + + // const fileData = await tabDoc.getFileData({ + // includeAnnotations: true, + // }); + + // console.log({ fileData }); + + // const blob = new Blob([fileData], { + // type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + // }); + + // // 다운로드 + // // const link = document.createElement("a"); + // // link.href = URL.createObjectURL(blob); + // // link.download = fileName; + // // document.body.appendChild(link); + // // link.click(); + // // document.body.removeChild(link); + // } + } + }; + + return ( + 0} onOpenChange={onClose}> + + + Report + + 사용하시고자 하는 Report Template를 선택하여 주시기 바랍니다. + + +
+ + +
+
+ +
+ + + + +
+
+ ); +}; + +interface ReportWebViewerProps { + columnsJSON: DataTableColumnJSON[]; + reportTempPath: string; + reportDatas: ReportData[]; + instance: null | WebViewerInstance; + setInstance: Dispatch>; + setFileLoading: Dispatch>; +} + +const ReportWebViewer: FC = ({ + columnsJSON, + reportTempPath, + reportDatas, + instance, + setInstance, + setFileLoading, +}) => { + const [viwerLoading, setViewerLoading] = useState(true); + const viewer = useRef(null); + const initialized = React.useRef(false); + const isCancelled = React.useRef(false); // 초기화 중단용 flag + + useEffect(() => { + if (!initialized.current) { + initialized.current = true; + isCancelled.current = false; // 다시 열릴 때는 false로 리셋 + + requestAnimationFrame(() => { + if (viewer.current) { + import("@pdftron/webviewer").then(({ default: WebViewer }) => { + console.log(isCancelled.current); + if (isCancelled.current) { + console.log("📛 WebViewer 초기화 취소됨 (Dialog 닫힘)"); + + return; + } + + WebViewer( + { + path: "/pdftronWeb", + licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, + fullAPI: true, + }, + viewer.current as HTMLDivElement + ).then(async (instance: WebViewerInstance) => { + setInstance(instance); + // //Tab 메뉴 사용 필요시 활성화 + // instance.UI.enableFeatures([instance.UI.Feature.MultiTab]); + // instance.UI.disableElements([ + // "addTabButton", + // "multiTabsEmptyPage", + // ]); + setViewerLoading(false); + }); + }); + } + }); + } + + return () => { + // cleanup 시에는 중단 flag 세움 + if (instance) { + instance.UI.dispose(); + } + setTimeout(() => cleanupHtmlStyle(), 500); + }; + }, []); + + useEffect(() => { + importReportData( + columnsJSON, + instance, + reportDatas, + reportTempPath, + setFileLoading + ); + }, [reportTempPath, reportDatas, instance, columnsJSON]); + + return ( +
+ {viwerLoading && ( +
+ +

문서 뷰어 로딩 중...

+
+ )} +
+ ); +}; + +const cleanupHtmlStyle = () => { + const htmlElement = document.documentElement; + + // 기존 style 속성 가져오기 + const originalStyle = htmlElement.getAttribute("style") || ""; + + // "color-scheme: light" 또는 "color-scheme: dark" 찾기 + const colorSchemeStyle = originalStyle + .split(";") + .map((s) => s.trim()) + .find((s) => s.startsWith("color-scheme:")); + + // 새로운 스타일 적용 (color-scheme만 유지) + if (colorSchemeStyle) { + htmlElement.setAttribute("style", colorSchemeStyle + ";"); + } else { + htmlElement.removeAttribute("style"); // color-scheme도 없으면 style 속성 자체 삭제 + } + + console.log("html style 삭제"); +}; + +const stringifyAllValues = (obj: any): any => { + if (Array.isArray(obj)) { + return obj.map((item) => stringifyAllValues(item)); + } else if (typeof obj === "object" && obj !== null) { + const result: any = {}; + for (const key in obj) { + result[key] = stringifyAllValues(obj[key]); + } + return result; + } else { + return obj !== null && obj !== undefined ? String(obj) : ""; + } +}; + +type ImportReportData = ( + columnsJSON: DataTableColumnJSON[], + instance: null | WebViewerInstance, + reportDatas: ReportData[], + reportTempPath: string, + setFileLoading: Dispatch> +) => void; + +const importReportData: ImportReportData = async ( + columnsJSON, + instance, + reportDatas, + reportTempPath, + setFileLoading +) => { + setFileLoading(true); + try { + if (instance && reportDatas.length > 0 && reportTempPath.length > 0) { + const { UI, Core } = instance; + const { documentViewer, createDocument } = Core; + + const getFileData = await fetch(reportTempPath); + const reportFileBlob = await getFileData.blob(); + + const reportData = reportDatas[0]; + const reportValue = stringifyAllValues(reportData); + + const reportValueMapping: { [key: string]: any } = {}; + + columnsJSON.forEach((c) => { + const { key, label } = c; + + const objKey = label.split(" ").join("_"); + + reportValueMapping[objKey] = reportValue[key]; + }); + + const doc = await createDocument(reportFileBlob, { + extension: "docx", + }); + + await doc.applyTemplateValues(reportValueMapping); + + documentViewer.loadDocument(doc, { + extension: "docx", + enableOfficeEditing: true, + officeOptions: { + formatOptions: { + applyPageBreaksToSheet: true, + }, + }, + }); + } + } catch (err) { + } finally { + setFileLoading(false); + } +}; + +const importReportDataTab: ImportReportData = async ( + columnJSON, + instance, + reportDatas, + reportTempPath, + setFileLoading +) => { + setFileLoading(true); + try { + if (instance && reportDatas.length > 0 && reportTempPath.length > 0) { + const { UI, Core } = instance; + const { createDocument } = Core; + + const getFileData = await fetch(reportTempPath); + const reportFileBlob = await getFileData.blob(); + + const prevTab = UI.TabManager.getAllTabs(); + + (prevTab as object[] as { id: number }[]).forEach((c) => { + const { id } = c; + UI.TabManager.deleteTab(id); + }); + + const fileOptions = reportDatas.map((c) => { + const { tagNumber } = c; + + const options = { + filename: `${tagNumber}_report.docx`, + }; + + return { options, reportData: c }; + }); + + const tabIds = []; + + for (const fileOption of fileOptions) { + let doc = await createDocument(reportFileBlob, { + ...fileOption.options, + extension: "docx", + }); + + await doc.applyTemplateValues( + stringifyAllValues(fileOption.reportData) + ); + + const tab = await UI.TabManager.addTab(doc, { + ...fileOption.options, + }); + + tabIds.push(tab); // 탭 ID 저장 + } + + if (tabIds.length > 0) { + await UI.TabManager.setActiveTab(tabIds[0]); + } + } + } catch (err) { + } finally { + setFileLoading(false); + } +}; + +type UpdateReportTempList = ( + packageId: number, + formId: number, + setPrevReportTemp: Dispatch> +) => void; + +const updateReportTempList: UpdateReportTempList = async ( + packageId, + formId, + setTempList +) => { + const tempList = await getReportTempList(packageId, formId); + + setTempList( + tempList.map((c) => { + const { fileName, filePath } = c; + return { fileName, filePath }; + }) + ); +}; diff --git a/components/form-data/form-data-report-temp-upload-dialog.tsx b/components/form-data/form-data-report-temp-upload-dialog.tsx new file mode 100644 index 00000000..b646c3e6 --- /dev/null +++ b/components/form-data/form-data-report-temp-upload-dialog.tsx @@ -0,0 +1,305 @@ +"use client"; + +import React, { + FC, + Dispatch, + SetStateAction, + useState, + useEffect, +} from "react"; +import { useToast } from "@/hooks/use-toast"; +import prettyBytes from "pretty-bytes"; +import { X, Loader2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { + Dropzone, + DropzoneDescription, + DropzoneInput, + DropzoneTitle, + DropzoneUploadIcon, + DropzoneZone, +} from "@/components/ui/dropzone"; +import { + FileList, + FileListAction, + FileListDescription, + FileListHeader, + FileListIcon, + FileListInfo, + FileListItem, + FileListName, +} from "@/components/ui/file-list"; +import { getReportTempList, uploadReportTemp } from "@/lib/forms/services"; +import { VendorDataReportTemps } from "@/db/schema/vendorData"; + +interface FormDataReportTempUploadDialogProps { + open: boolean; + setOpen: Dispatch>; + packageId: number; + formCode: string; + formId: number; + uploaderType: string; +} + +// 최대 파일 크기 설정 (3000MB) +const MAX_FILE_SIZE = 3000000; + +export const FormDataReportTempUploadDialog: FC< + FormDataReportTempUploadDialogProps +> = ({ open, setOpen, packageId, formId, uploaderType }) => { + const { toast } = useToast(); + const [selectedFiles, setSelectedFiles] = useState([]); + const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const [prevReportTemp, setPrevReportTemp] = useState( + [] + ); + + useEffect(() => { + updateReportTempList(packageId, formId, setPrevReportTemp); + }, [packageId, formId]); + + // 드롭존 - 파일 드랍 처리 + const handleDropAccepted = (acceptedFiles: File[]) => { + const newFiles = [...selectedFiles, ...acceptedFiles]; + setSelectedFiles(newFiles); + }; + + // 드롭존 - 파일 거부(에러) 처리 + const handleDropRejected = (fileRejections: any[]) => { + fileRejections.forEach((rejection) => { + toast({ + variant: "destructive", + title: "File Error", + description: `${rejection.file.name}: ${ + rejection.errors[0]?.message || "Upload failed" + }`, + }); + }); + }; + + // 파일 제거 핸들러 + const removeFile = (index: number) => { + const updatedFiles = [...selectedFiles]; + updatedFiles.splice(index, 1); + setSelectedFiles(updatedFiles); + }; + + const submitData = async () => { + setIsUploading(true); + setUploadProgress(0); + try { + const totalFiles = selectedFiles.length; + let successCount = 0; + + for (let i = 0; i < totalFiles; i++) { + const file = selectedFiles[i]; + + const formData = new FormData(); + formData.append("file", file); + formData.append("customFileName", file.name); + formData.append("uploaderType", uploaderType); + + await uploadReportTemp(packageId, formId, formData); + + successCount++; + setUploadProgress(Math.round((successCount / totalFiles) * 100)); + } + } catch (err) { + console.error(err); + toast({ + title: "Error", + description: "파일 업로드 중 오류가 발생했습니다.", + variant: "destructive", + }); + } finally { + setIsUploading(false); + setUploadProgress(0); + updateReportTempList(packageId, formId, setPrevReportTemp); + setOpen(false); + } + }; + + return ( + + + + Report Template Upload + + 사용하시고자 하는 Report Template를 업로드 하여주시기 바랍니다. + + + {/* {prevReportTemp.length > 0 && ( + <> + + + {prevReportTemp.map((c, i) => { + return
{i}
; + })} +
+ + )} */} + + + {({ maxSize }) => ( + <> + + +
+ +
+ 파일을 여기에 드롭하세요 + + 또는 클릭하여 파일을 선택하세요. 최대 크기:{" "} + {maxSize ? prettyBytes(maxSize) : "무제한"} + +
+
+
+ + + )} +
+ + {selectedFiles.length > 0 && ( +
+
+
+ 선택된 파일 ({selectedFiles.length}) +
+ {selectedFiles.length}개 파일 +
+ + + +
+ )} + + {isUploading && } + + + + +
+
+ ); +}; + +interface UploadFileItemProps { + selectedFiles: File[]; + removeFile: (index: number) => void; + isUploading: boolean; +} + +const UploadFileItem: FC = ({ + selectedFiles, + removeFile, + isUploading, +}) => { + return ( + + {selectedFiles.map((file, index) => ( + + + + + {file.name} + + {prettyBytes(file.size)} + + + removeFile(index)} + disabled={isUploading} + > + + Remove + + + + ))} + + ); +}; + +const UploadProgressBox: FC<{ uploadProgress: number }> = ({ + uploadProgress, +}) => { + return ( +
+
+ + {uploadProgress}% 업로드 중... +
+
+
+
+
+ ); +}; + +const generateFileName = ( + packageId: number, + formId: number, + originalFileName: string, + index: number, + totalFiles: number +) => { + // Get the file extension + const extension = originalFileName.split(".").pop() || ""; + + // Base name without extension + const baseName = `${packageId}_${formId}`; + + // For multiple files, add a suffix + if (totalFiles > 1) { + return `${baseName}_${index + 1}.${extension}`; + } + + // For a single file, no suffix needed + return `${baseName}.${extension}`; +}; + +type UpdateReportTempList = ( + packageId: number, + formId: number, + setPrevReportTemp: Dispatch> +) => void; + +const updateReportTempList: UpdateReportTempList = async ( + packageId, + formId, + setPrevReportTemp +) => { + const tempList = await getReportTempList(packageId, formId); + setPrevReportTemp(tempList); +}; diff --git a/components/form-data/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx index d44616f8..38f5cad8 100644 --- a/components/form-data/form-data-table-columns.tsx +++ b/components/form-data/form-data-table-columns.tsx @@ -27,24 +27,27 @@ export type ColumnType = "STRING" | "NUMBER" | "LIST" export interface DataTableColumnJSON { - key: string + key: string; /** 실제 Excel 등에서 구분용으로 쓰이는 label (고정) */ label: string /** UI 표시용 label (예: 단위를 함께 표시) */ displayLabel?: string - type: ColumnType - options?: string[] - uom?: string + type: ColumnType; + options?: string[]; + uom?: string; } -/** - * getColumns 함수에 필요한 props +/** + * getColumns 함수에 필요한 props * - TData: 테이블에 표시할 행(Row)의 타입 */ interface GetColumnsProps { - columnsJSON: DataTableColumnJSON[] - setRowAction: React.Dispatch | null>> + columnsJSON: DataTableColumnJSON[]; + setRowAction: React.Dispatch< + React.SetStateAction | null> + >; + setReportData: React.Dispatch>; } /** @@ -55,8 +58,8 @@ interface GetColumnsProps { export function getColumns({ columnsJSON, setRowAction, + setReportData, }: GetColumnsProps): ColumnDef[] { - // (1) 기본 컬럼들 const baseColumns: ColumnDef[] = columnsJSON.map((col) => ({ accessorKey: col.key, @@ -71,7 +74,7 @@ export function getColumns({ excelHeader: col.label, minWidth: 80, paddingFactor: 1.2, - maxWidth: col.key ==="tagNumber"?120:150, + maxWidth: col.key === "tagNumber" ? 120 : 150, }, // (2) 실제 셀(cell) 렌더링: type에 따라 분기 가능 cell: ({ row }) => { @@ -81,7 +84,9 @@ export function getColumns({ switch (col.type) { case "NUMBER": // 예: number인 경우 콤마 등 표시 - return
{cellValue ? Number(cellValue).toLocaleString() : ""}
+ return ( +
{cellValue ? Number(cellValue).toLocaleString() : ""}
+ ); // case "date": // // 예: 날짜 포맷팅 @@ -108,30 +113,38 @@ export function getColumns({ header: "", cell: ({ row }) => ( - - - - - setRowAction({ row, type: "update" })} - > - Edit - - - + + + + + setRowAction({ row, type: "update" })} + > + Edit + + { + const { original } = row; + setReportData([original]); + }} + > + Create Report + + + ), - size:40, - meta:{ - maxWidth:40 + size: 40, + meta: { + // maxWidth: 40, }, enablePinning: true, - } + }; // (4) 최종 반환 return [...baseColumns, actionColumn] diff --git a/config/poColumnsConfig.ts b/config/poColumnsConfig.ts index 6466f3e1..606c6ab4 100644 --- a/config/poColumnsConfig.ts +++ b/config/poColumnsConfig.ts @@ -51,6 +51,14 @@ export const poColumnsConfig: PoColumnConfig[] = [ group: "Basic Info", type: "text", }, + { + id: "esignStatus", // db에 없는 가상 컬럼 + label: "E-Sign Status", + excelHeader: "E-Sign Status", + group: "Basic Info", + type: "custom", // 커스텀 렌더링이 필요함을 나타냄 + customType: "esignStatus", // 구분이 필요한 경우 추가 필드 + }, { id: "startDate", label: "Start Date", diff --git a/db/migrations/0103_huge_wallflower.sql b/db/migrations/0103_huge_wallflower.sql new file mode 100644 index 00000000..b108d28f --- /dev/null +++ b/db/migrations/0103_huge_wallflower.sql @@ -0,0 +1,37 @@ +DROP VIEW "public"."contracts_detail_view";--> statement-breakpoint +CREATE VIEW "public"."contracts_detail_view" AS (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 ( + SELECT 1 + FROM "contract_envelopes" + WHERE "contract_envelopes"."contract_id" = "contracts"."id" + ) as "has_signature", COALESCE(( + SELECT json_agg( + json_build_object( + 'id', ce.id, + 'envelopeId', ce.envelope_id, + 'documentId', ce.document_id, + 'envelopeStatus', ce.envelope_status, + 'fileName', ce.file_name, + 'filePath', ce.file_path, + 'createdAt', ce.created_at, + 'updatedAt', ce.updated_at, + 'signers', ( + SELECT json_agg( + json_build_object( + 'id', cs.id, + 'vendorContactId', cs.vendor_contact_id, + 'signerType', cs.signer_type, + 'signerEmail', cs.signer_email, + 'signerName', cs.signer_name, + 'signerPosition', cs.signer_position, + 'signerStatus', cs.signer_status, + 'signedAt', cs.signed_at + ) + ) + FROM "contract_signers" AS cs + WHERE cs.envelope_id = ce.id + ) + ) + ) + FROM "contract_envelopes" AS ce + WHERE ce.contract_id = "contracts"."id" + ), '[]') as "envelopes" from "contracts" left join "projects" on "contracts"."project_id" = "projects"."id" left join "vendors" on "contracts"."vendor_id" = "vendors"."id"); \ No newline at end of file diff --git a/db/migrations/meta/0103_snapshot.json b/db/migrations/meta/0103_snapshot.json new file mode 100644 index 00000000..a334d505 --- /dev/null +++ b/db/migrations/meta/0103_snapshot.json @@ -0,0 +1,4757 @@ +{ + "id": "c61340a1-8aed-47e6-8d5b-7528e1a5538f", + "prevId": "1f9424b7-fa9e-44bf-a5d1-dbceb2d9da56", + "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" + } + }, + "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.items": { + "name": "items", + "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 + }, + "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.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.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_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 + }, + "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": "no action" + }, + "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": "no action" + } + }, + "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.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_cbe_evaluations_id_fk": { + "name": "rfq_comments_cbe_id_cbe_evaluations_id_fk", + "tableFrom": "rfq_comments", + "tableTo": "cbe_evaluations", + "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 + }, + "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_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 + }, + "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 + }, + "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.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, + "default": "concat('TASK-', to_char(nextval('tasks_code_seq'), 'FM0000'))" + }, + "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.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 + }, + "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 + } + }, + "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" + } + }, + "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 + }, + "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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "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.form_templates": { + "name": "form_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "form_id": { + "name": "form_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 + }, + "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": { + "form_templates_form_id_forms_id_fk": { + "name": "form_templates_form_id_forms_id_fk", + "tableFrom": "form_templates", + "tableTo": "forms", + "columnsFrom": [ + "form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + } + }, + "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_tag_type_code_tag_types_code_fk": { + "name": "tag_classes_tag_type_code_tag_types_code_fk", + "tableFrom": "tag_classes", + "tableTo": "tag_types", + "columnsFrom": [ + "tag_type_code" + ], + "columnsTo": [ + "code" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag_subfield_options": { + "name": "tag_subfield_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "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_attributes_id_tag_subfields_attributes_id_fk": { + "name": "tag_subfield_options_attributes_id_tag_subfields_attributes_id_fk", + "tableFrom": "tag_subfield_options", + "tableTo": "tag_subfields", + "columnsFrom": [ + "attributes_id" + ], + "columnsTo": [ + "attributes_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag_subfields": { + "name": "tag_subfields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "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_tag_type_code_tag_types_code_fk": { + "name": "tag_subfields_tag_type_code_tag_types_code_fk", + "tableFrom": "tag_subfields", + "tableTo": "tag_types", + "columnsFrom": [ + "tag_type_code" + ], + "columnsTo": [ + "code" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uniq_tag_type_attribute": { + "name": "uniq_tag_type_attribute", + "nullsNotDistinct": false, + "columns": [ + "tag_type_code", + "attributes_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 + }, + "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 + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag_types": { + "name": "tag_types", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": true, + "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": {}, + "compositePrimaryKeys": {}, + "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": {}, + "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.view_tag_subfields": { + "name": "view_tag_subfields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "tag_type_code": { + "name": "tag_type_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "tag_type_description": { + "name": "tag_type_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "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 + }, + "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 + } + }, + "contract_id": { + "name": "contract_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, + "default": "'ACTIVE'" + }, + "issued_date": { + "name": "issued_date", + "type": "date", + "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": {} + } + }, + "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 + }, + "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": {} + } + }, + "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 + }, + "comment": { + "name": "comment", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "approved_date": { + "name": "approved_date", + "type": "date", + "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_rev": { + "name": "unique_stage_rev", + "columns": [ + { + "expression": "issue_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "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_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.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'" + }, + "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()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "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', 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.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_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\", \"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\", \"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 \"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.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\n (\n SELECT COUNT(*)\n FROM issue_stages\n WHERE document_id = d.id\n ) AS stage_count,\n\n COALESCE( \n (\n SELECT json_agg(i.stage_name)\n FROM issue_stages i\n WHERE i.document_id = d.id\n ), \n '[]'\n ) AS stage_list,\n\n d.created_at,\n d.updated_at\n FROM documents d\n", + "name": "document_stages_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 + }, + "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.status,\n d.issued_date,\n \n d.contract_id,\n \n (\n SELECT id FROM issue_stages\n WHERE document_id = d.id\n ORDER BY created_at DESC LIMIT 1\n ) AS latest_stage_id,\n (\n SELECT stage_name FROM issue_stages\n WHERE document_id = d.id\n ORDER BY created_at DESC LIMIT 1\n ) AS latest_stage_name,\n (\n SELECT plan_date FROM issue_stages\n WHERE document_id = d.id\n ORDER BY created_at DESC LIMIT 1\n ) AS latest_stage_plan_date,\n (\n SELECT actual_date FROM issue_stages\n WHERE document_id = d.id\n ORDER BY created_at DESC LIMIT 1\n ) AS latest_stage_actual_date,\n \n (\n SELECT r.id FROM revisions r\n JOIN issue_stages i ON r.issue_stage_id = i.id\n WHERE i.document_id = d.id\n ORDER BY r.created_at DESC LIMIT 1\n ) AS latest_revision_id,\n (\n SELECT r.revision FROM revisions r\n JOIN issue_stages i ON r.issue_stage_id = i.id\n WHERE i.document_id = d.id\n ORDER BY r.created_at DESC LIMIT 1\n ) AS latest_revision,\n (\n SELECT r.uploader_type FROM revisions r\n JOIN issue_stages i ON r.issue_stage_id = i.id\n WHERE i.document_id = d.id\n ORDER BY r.created_at DESC LIMIT 1\n ) AS latest_revision_uploader_type,\n (\n SELECT r.uploader_name FROM revisions r\n JOIN issue_stages i ON r.issue_stage_id = i.id\n WHERE i.document_id = d.id\n ORDER BY r.created_at DESC LIMIT 1\n ) AS latest_revision_uploader_name,\n \n (\n SELECT COUNT(*) FROM document_attachments a\n JOIN revisions r ON a.revision_id = r.id\n JOIN issue_stages i ON r.issue_stage_id = i.id\n WHERE i.document_id = d.id\n ) 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_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 + } + }, + "_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 708f1976..26a15dcf 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -722,6 +722,13 @@ "when": 1742964981809, "tag": "0102_melodic_blob", "breakpoints": true + }, + { + "idx": 103, + "version": "7", + "when": 1743043253528, + "tag": "0103_huge_wallflower", + "breakpoints": true } ] } \ No newline at end of file diff --git a/db/schema/contract.ts b/db/schema/contract.ts index 3c29f0d0..10721b4d 100644 --- a/db/schema/contract.ts +++ b/db/schema/contract.ts @@ -135,31 +135,31 @@ export const contractEnvelopes = pgTable("contract_envelopes", { // 하나의 Envelope에 여러 서명자(사인 요청 대상)가 있을 수 있음 export const contractSigners = pgTable("contract_signers", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), - + // Envelope와 1:N 관계 envelopeId: integer("envelope_id") .notNull() .references(() => contractEnvelopes.id, { onDelete: "cascade" }), - + // Reference to vendor_contacts table (optional - if signer is from vendor contacts) vendorContactId: integer("vendor_contact_id") .references(() => vendorContacts.id), - + // Is this signer from the requester (company) side or vendor side - signerType: varchar("signer_type", { + signerType: varchar("signer_type", { length: 20, enum: ["REQUESTER", "VENDOR"] }).notNull().default("VENDOR"), - + // 서명자 정보 (manual entry or populated from vendor contact) signerEmail: varchar("signer_email", { length: 255 }).notNull(), signerName: varchar("signer_name", { length: 100 }).notNull(), signerPosition: varchar("signer_position", { length: 100 }), - + // 서명자별 상태 (sent, delivered, signed, declined, etc.) signerStatus: varchar("signer_status", { length: 50 }).default("PENDING"), signedAt: timestamp("signed_at"), - + // 생성/수정 시각 createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), @@ -210,13 +210,46 @@ export const contractsDetailView = pgView("contracts_detail_view").as((qb) => { // Timestamps createdAt: contracts.createdAt, updatedAt: contracts.updatedAt, - + // Electronic signature status - ADDED .as('has_signature') here hasSignature: sql`EXISTS ( SELECT 1 FROM ${contractEnvelopes} WHERE ${contractEnvelopes.contractId} = ${contracts.id} )`.as('has_signature'), + // --- 전자서명 이력 (Envelope) + 서명자(Signer)를 JSON 으로 중첩한 배열 --- + envelopes: sql`COALESCE(( + SELECT json_agg( + json_build_object( + 'id', ce.id, + 'envelopeId', ce.envelope_id, + 'documentId', ce.document_id, + 'envelopeStatus', ce.envelope_status, + 'fileName', ce.file_name, + 'filePath', ce.file_path, + 'createdAt', ce.created_at, + 'updatedAt', ce.updated_at, + 'signers', ( + SELECT json_agg( + json_build_object( + 'id', cs.id, + 'vendorContactId', cs.vendor_contact_id, + 'signerType', cs.signer_type, + 'signerEmail', cs.signer_email, + 'signerName', cs.signer_name, + 'signerPosition', cs.signer_position, + 'signerStatus', cs.signer_status, + 'signedAt', cs.signed_at + ) + ) + FROM ${contractSigners} AS cs + WHERE cs.envelope_id = ce.id + ) + ) + ) + FROM ${contractEnvelopes} AS ce + WHERE ce.contract_id = ${contracts.id} + ), '[]')`.as("envelopes") }) .from(contracts) .leftJoin(projects, eq(contracts.projectId, projects.id)) diff --git a/lib/po/table/po-table-columns.tsx b/lib/po/table/po-table-columns.tsx index c2c01136..a13b2acf 100644 --- a/lib/po/table/po-table-columns.tsx +++ b/lib/po/table/po-table-columns.tsx @@ -91,16 +91,77 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef[] } const groupMap: Record[]> = {}; - poColumnsConfig.forEach((cfg) => { - // Use "_noGroup" if no group is specified - const groupName = cfg.group || "_noGroup"; + // (1) JSON config를 읽어서 ColumnDef를 생성하는 부분 (일부 발췌) +poColumnsConfig.forEach((cfg) => { + const groupName = cfg.group || "_noGroup" + if (!groupMap[groupName]) { + groupMap[groupName] = [] + } - if (!groupMap[groupName]) { - groupMap[groupName] = []; - } + let childCol: ColumnDef + + if (cfg.type === "custom" && cfg.customType === "esignStatus") { + // ======================================== + // (2) 전자서명 전용 커스텀 컬럼 + // ======================================== + childCol = { + id: cfg.id, + header: ({ column }) => ( + + ), + // 여기서 row.original.envelopes 등 활용하여 최신 전자서명 상태 표시 + cell: ({ row }) => { + const data = row.original + if (!data.envelopes || data.envelopes.length === 0) { + return ( +
+ No E-Sign +
+ ) + } + + // envelopes가 여러 개 있으면 최신(가장 최근 updatedAt) 가져오기 + const sorted = [...data.envelopes].sort((a, b) => { + const dateA = new Date(a.updatedAt) + const dateB = new Date(b.updatedAt) + return dateB.getTime() - dateA.getTime() + }) + const latest = sorted[0] - // Child column definition - const childCol: ColumnDef = { + // 상태에 따라 다른 UI 색상/아이콘 + const status = latest.envelopeStatus // "sent", "completed", ... + const colorMap: Record = { + completed: "text-green-600", + sent: "text-blue-600", + voided: "text-red-600", + // ... + } + const colorClass = colorMap[status] || "text-gray-700" + + return ( + + ) + }, + meta: { + excelHeader: cfg.excelHeader, + group: cfg.group, + type: cfg.type, + }, + } + } else { + // ======================================== + // (3) 일반 컬럼 (type: text/date/number 등) + // ======================================== + childCol = { accessorKey: cfg.id, enableResizing: true, header: ({ column }) => ( @@ -112,17 +173,19 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef { - if (cfg.id === "createdAt" || cfg.id === "updatedAt") { - const dateVal = cell.getValue() as Date; - return formatDate(dateVal); + // 날짜 포맷, 숫자 포맷 등 처리 + if (cfg.type === "date") { + const dateVal = cell.getValue() as Date + return formatDate(dateVal) } - - return row.getValue(cfg.id) ?? ""; + // ... + return row.getValue(cfg.id) ?? "" }, - }; + } + } - groupMap[groupName].push(childCol); - }); + groupMap[groupName].push(childCol) +}) // ---------------------------------------------------------------- // 3-2) Create actual parent columns (groups) from the groupMap diff --git a/lib/rfqs/table/rfqs-table.tsx b/lib/rfqs/table/rfqs-table.tsx index 7d996816..48c04930 100644 --- a/lib/rfqs/table/rfqs-table.tsx +++ b/lib/rfqs/table/rfqs-table.tsx @@ -213,7 +213,7 @@ export function RfqsTable({ promises, rfqType = RfqType.PURCHASE }: RfqsTablePro }) return ( -
+
} diff --git a/lib/vendors/table/request-vendor-pg-dialog.tsx b/lib/vendors/table/request-vendor-pg-dialog.tsx index b417f846..de23ad9b 100644 --- a/lib/vendors/table/request-vendor-pg-dialog.tsx +++ b/lib/vendors/table/request-vendor-pg-dialog.tsx @@ -70,7 +70,7 @@ export function RequestPQVendorsDialog({ ) : null} @@ -114,7 +114,7 @@ export function RequestPQVendorsDialog({ ) : null} -- cgit v1.2.3