summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/evcp/poa/page.tsx61
-rw-r--r--components/documents/StageList.tsx2
-rw-r--r--components/documents/view-document-dialog.tsx265
-rw-r--r--components/form-data/form-data-report-batch-dialog.tsx74
-rw-r--r--components/form-data/form-data-report-dialog.tsx139
-rw-r--r--components/form-data/form-data-report-temp-upload-dialog.tsx352
-rw-r--r--components/form-data/form-data-table.tsx47
-rw-r--r--config/poaColumnsConfig.ts131
-rw-r--r--db/migrations/0097_poa_initial_setup.sql95
-rw-r--r--db/schema/contract.ts104
-rw-r--r--db/schema/vendorData.ts321
-rw-r--r--db/seeds_2/poaSeed.ts109
-rw-r--r--lib/forms/services.ts61
-rw-r--r--lib/pdftron/serverSDK/createReport.ts83
-rw-r--r--lib/po/service.ts4
-rw-r--r--lib/poa/service.ts132
-rw-r--r--lib/poa/table/poa-table-columns.tsx165
-rw-r--r--lib/poa/table/poa-table-toolbar-actions.tsx45
-rw-r--r--lib/poa/table/poa-table.tsx189
-rw-r--r--lib/poa/validations.ts66
-rw-r--r--pages/api/pdftron/createVendorDataReports.ts78
-rw-r--r--pages/api/po/sendDocuSign.ts8
-rw-r--r--pages/api/po/webhook.ts29
-rw-r--r--public/vendorFormReportSample/sample_template_file.docxbin0 -> 24773 bytes
24 files changed, 962 insertions, 1598 deletions
diff --git a/app/[lng]/evcp/poa/page.tsx b/app/[lng]/evcp/poa/page.tsx
deleted file mode 100644
index dec5e05b..00000000
--- a/app/[lng]/evcp/poa/page.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import * as React from "react"
-import { type SearchParams } from "@/types/table"
-
-import { getValidFilters } from "@/lib/data-table"
-import { Skeleton } from "@/components/ui/skeleton"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-import { Shell } from "@/components/shell"
-import { getChangeOrders } from "@/lib/poa/service"
-import { searchParamsCache } from "@/lib/poa/validations"
-import { ChangeOrderListsTable } from "@/lib/poa/table/poa-table"
-
-interface IndexPageProps {
- searchParams: Promise<SearchParams>
-}
-
-export default async function IndexPage(props: IndexPageProps) {
- const searchParams = await props.searchParams
- const search = searchParamsCache.parse(searchParams)
-
- const validFilters = getValidFilters(search.filters)
-
- const promises = Promise.all([
- getChangeOrders({
- ...search,
- filters: validFilters,
- }),
- ])
-
- return (
- <Shell className="gap-2">
- <div className="flex items-center justify-between space-y-2">
- <div className="flex items-center justify-between space-y-2">
- <div>
- <h2 className="text-2xl font-bold tracking-tight">
- 변경 PO 확인 및 전자서명
- </h2>
- <p className="text-muted-foreground">
- 발행된 PO의 변경 내역을 확인하고 관리할 수 있습니다.
- </p>
- </div>
- </div>
- </div>
-
- <React.Suspense fallback={<Skeleton className="h-7 w-52" />}>
- </React.Suspense>
- <React.Suspense
- fallback={
- <DataTableSkeleton
- columnCount={6}
- searchableColumnCount={1}
- filterableColumnCount={2}
- cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <ChangeOrderListsTable promises={promises} />
- </React.Suspense>
- </Shell>
- )
-} \ No newline at end of file
diff --git a/components/documents/StageList.tsx b/components/documents/StageList.tsx
index 81f8a5ca..6df448df 100644
--- a/components/documents/StageList.tsx
+++ b/components/documents/StageList.tsx
@@ -55,7 +55,7 @@ interface Version {
approvedDate: string | null
DocumentSubmitDate: Date
attachments: Attachment[]
- selected?: boolean
+ selected?: boolean;
}
export default function StageList({ document }: StageListProps) {
diff --git a/components/documents/view-document-dialog.tsx b/components/documents/view-document-dialog.tsx
index 6daa806b..bd802b77 100644
--- a/components/documents/view-document-dialog.tsx
+++ b/components/documents/view-document-dialog.tsx
@@ -9,78 +9,80 @@ import {
import { Building2, FileIcon, Loader2 } from "lucide-react"
import { Button } from "@/components/ui/button"
+// 인터페이스
interface Attachment {
id: number;
fileName: string;
filePath: string;
fileType?: string;
}
-
interface Version {
- id: number
- stage: string
- revision: string
- uploaderType: string
- uploaderName: string | null
- comment: string | null
- status: string | null
- planDate: string | null
- actualDate: string | null
- approvedDate: string | null
- DocumentSubmitDate: Date
- attachments: Attachment[]
- selected: boolean
+ id: number;
+ stage: string;
+ revision: string;
+ uploaderType: string;
+ uploaderName: string | null;
+ comment: string | null;
+ status: string | null;
+ planDate: string | null;
+ actualDate: string | null;
+ approvedDate: string | null;
+ DocumentSubmitDate: Date;
+ attachments: Attachment[];
+ selected?: boolean;
}
type ViewDocumentDialogProps = {
- versions: Version[]
-}
+ versions: Version[];
+};
-export function ViewDocumentDialog({versions}: ViewDocumentDialogProps){
- const [open, setOpen] = React.useState(false)
-
+export function ViewDocumentDialog({ versions }: ViewDocumentDialogProps) {
+ const [open, setOpen] = React.useState(false);
return (
<>
- <Button
- size="sm"
- className="border-blue-200"
- variant="outline"
- onClick={() => setOpen(prev => !prev)}
- >
- 문서 보기
- </Button>
- {open && <DocumentViewer
- open={open}
- setOpen={setOpen}
- versions={versions}
- />
- }
- </>
+ <Button
+ size="sm"
+ className="border-blue-200"
+ variant="outline"
+ onClick={() => setOpen((prev) => !prev)}
+ >
+ 문서 보기
+ </Button>
+ {open && (
+ <DocumentViewer open={open} setOpen={setOpen} versions={versions} />
+ )}
+ </>
);
}
-function DocumentViewer({open, setOpen, versions}){
- const [instance, setInstance] = React.useState<null | WebViewerInstance>(null)
- const [viwerLoading, setViewerLoading] = React.useState<boolean>(true)
- const [fileSetLoading, setFileSetLoading] = React.useState<boolean>(true)
+const DocumentViewer: React.FC<{
+ open: boolean;
+ setOpen: React.Dispatch<React.SetStateAction<boolean>>;
+ versions: Version[];
+}> = ({ open, setOpen, versions }) => {
+ const [instance, setInstance] = React.useState<null | WebViewerInstance>(
+ null
+ );
+ const [viwerLoading, setViewerLoading] = React.useState<boolean>(true);
+ const [fileSetLoading, setFileSetLoading] = React.useState<boolean>(true);
const viewer = React.useRef<HTMLDivElement>(null);
const initialized = React.useRef(false);
const isCancelled = React.useRef(false); // 초기화 중단용 flag
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 + ";");
@@ -88,146 +90,151 @@ function DocumentViewer({open, setOpen, versions}){
htmlElement.removeAttribute("style"); // color-scheme도 없으면 style 속성 자체 삭제
}
- console.log("html style 삭제")
+ console.log("html style 삭제");
};
React.useEffect(() => {
if (open && !initialized.current) {
initialized.current = true;
isCancelled.current = false; // 다시 열릴 때는 false로 리셋
-
+
requestAnimationFrame(() => {
if (viewer.current) {
import("@pdftron/webviewer").then(({ default: WebViewer }) => {
- console.log(isCancelled.current)
+ console.log(isCancelled.current);
if (isCancelled.current) {
console.log("📛 WebViewer 초기화 취소됨 (Dialog 닫힘)");
-
+
return;
}
-
+
WebViewer(
{
path: "/pdftronWeb",
- licenseKey: "demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd",
+ licenseKey:
+ "demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd",
fullAPI: true,
- css:"/globals.css"
+ css: "/globals.css",
},
viewer.current as HTMLDivElement
).then(async (instance: WebViewerInstance) => {
-
-
setInstance(instance);
instance.UI.enableFeatures([instance.UI.Feature.MultiTab]);
- instance.UI.disableElements(["addTabButton", "multiTabsEmptyPage"]);
+ instance.UI.disableElements([
+ "addTabButton",
+ "multiTabsEmptyPage",
+ ]);
setViewerLoading(false);
-
});
});
}
});
}
-
- return async () => {
+
+ return () => {
// cleanup 시에는 중단 flag 세움
- if(instance){
- await instance.UI.dispose()
+ if (instance) {
+ instance.UI.dispose();
}
- await setTimeout(() => cleanupHtmlStyle(), 500)
+ setTimeout(() => cleanupHtmlStyle(), 500);
};
}, [open]);
React.useEffect(() => {
- const loadDocument = async () => {
-
- if(instance && versions.length > 0){
- const { UI } = instance;
-
- const optionsArray = []
-
- versions.forEach(c => {
- const {attachments} = c
- attachments.forEach(c2 => {
- const {fileName, filePath, fileType} = c2
-
- const options = {
- filename: fileName,
- ...(fileType.includes("xlsx") && {
- officeOptions: {
- formatOptions: {
- applyPageBreaksToSheet: true,
+ const loadDocument = async () => {
+ if (instance && versions.length > 0) {
+ const { UI } = instance;
+
+ const optionsArray: any[] = [];
+
+ versions.forEach((c) => {
+ const { attachments } = c;
+ attachments.forEach((c2) => {
+ const { fileName, filePath, fileType } = c2;
+
+ const fileTypeCur = fileType ?? "";
+
+ const options = {
+ filename: fileName,
+ ...(fileTypeCur.includes("xlsx") && {
+ officeOptions: {
+ formatOptions: {
+ applyPageBreaksToSheet: true,
+ },
},
- },
- }),
- };
+ }),
+ };
- optionsArray.push({
- filePath,
- options
- })
- })
- })
+ optionsArray.push({
+ filePath,
+ options,
+ });
+ });
+ });
- const tabIds = [];
+ const tabIds = [];
- for (const option of optionsArray) {
- const { filePath, options } = option;
- const response = await fetch(filePath);
- const blob = await response.blob();
+ for (const option of optionsArray) {
+ const { filePath, options } = option;
+ const response = await fetch(filePath);
+ const blob = await response.blob();
- const tab = await UI.TabManager.addTab(blob, options);
- tabIds.push(tab); // 탭 ID 저장
- }
+ const tab = await UI.TabManager.addTab(blob, options);
+ tabIds.push(tab); // 탭 ID 저장
+ }
- if (tabIds.length > 0) {
- await UI.TabManager.setActiveTab(tabIds[0]);
- }
+ if (tabIds.length > 0) {
+ await UI.TabManager.setActiveTab(tabIds[0]);
+ }
- setFileSetLoading(false)
- }
- }
- loadDocument();
- }, [instance, versions])
+ setFileSetLoading(false);
+ }
+ };
+ loadDocument();
+ }, [instance, versions]);
-
return (
- <Dialog open={open} onOpenChange={async (val) => {
- console.log({val, fileSetLoading})
- if(!val && fileSetLoading){
- return;
- }
-
+ <Dialog
+ open={open}
+ onOpenChange={async (val) => {
+ if (!val && fileSetLoading) {
+ return;
+ }
+
if (instance) {
try {
await instance.UI.dispose();
- setInstance(null); // 상태도 초기화
-
+ setInstance(null); // 상태도 초기화
} catch (e) {
console.warn("dispose error", e);
}
}
-
+
// cleanupHtmlStyle()
- setViewerLoading(false);
- setOpen(prev => !prev)
- await setTimeout(() => cleanupHtmlStyle(), 1000)
- }}>
- <DialogContent className="w-[90vw] h-[90vh]" style={{maxWidth: "none"}}>
- <DialogHeader className="h-[38px]">
- <DialogTitle>
- 문서 미리보기
- </DialogTitle>
- <DialogDescription>
- 첨부파일 미리보기
- </DialogDescription>
+ setViewerLoading(false);
+ setOpen((prev) => !prev);
+ setTimeout(() => cleanupHtmlStyle(), 1000);
+ }}
+ >
+ <DialogContent className="w-[90vw] h-[90vh]" style={{ maxWidth: "none" }}>
+ <DialogHeader className="h-[38px]">
+ <DialogTitle>문서 미리보기</DialogTitle>
+ <DialogDescription>첨부파일 미리보기</DialogDescription>
</DialogHeader>
- <div ref={viewer} style={{height: "calc(90vh - 20px - 38px - 1rem - 48px)"}}>
- {viwerLoading && <div className="flex flex-col items-center justify-center py-12">
- <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
- <p className="text-sm text-muted-foreground">문서 뷰어 로딩 중...</p>
- </div>}
+ <div
+ ref={viewer}
+ style={{ height: "calc(90vh - 20px - 38px - 1rem - 48px)" }}
+ >
+ {viwerLoading && (
+ <div className="flex flex-col items-center justify-center py-12">
+ <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
+ <p className="text-sm text-muted-foreground">
+ 문서 뷰어 로딩 중...
+ </p>
+ </div>
+ )}
</div>
- </DialogContent>
+ </DialogContent>
</Dialog>
);
-} \ No newline at end of file
+};
diff --git a/components/form-data/form-data-report-batch-dialog.tsx b/components/form-data/form-data-report-batch-dialog.tsx
index e3fd7ea2..6c690363 100644
--- a/components/form-data/form-data-report-batch-dialog.tsx
+++ b/components/form-data/form-data-report-batch-dialog.tsx
@@ -6,11 +6,12 @@ import React, {
SetStateAction,
useState,
useEffect,
- useRef,
} from "react";
import { useToast } from "@/hooks/use-toast";
+import { toast as toastMessage} from "sonner";
import prettyBytes from "pretty-bytes";
import { X, Loader2 } from "lucide-react";
+import { saveAs } from 'file-saver';
import { Badge } from "@/components/ui/badge";
import {
Dialog,
@@ -25,7 +26,6 @@ import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
- SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
@@ -49,7 +49,7 @@ import {
FileListName,
} from "@/components/ui/file-list";
import { Button } from "@/components/ui/button";
-import { getReportTempList } from "@/lib/forms/services";
+import { getReportTempList, getOrigin } from "@/lib/forms/services";
import { DataTableColumnJSON } from "./form-data-table-columns";
const MAX_FILE_SIZE = 3000000;
@@ -129,30 +129,58 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
setIsUploading(true);
try {
- const totalFiles = selectedFiles.length;
- let successCount = 0;
+ const origin = await getOrigin()
- for (let i = 0; i < totalFiles; i++) {
- const file = selectedFiles[i];
+ const targetFiles = selectedFiles[0];
- const formData = new FormData();
- formData.append("file", file);
- formData.append("customFileName", file.name);
+ const reportDatas = reportData.map((c) => {
+ const reportValue = stringifyAllValues(c);
- // await uploadReportTemp(packageId, formId, formData);
+ const reportValueMapping: { [key: string]: any } = {};
- successCount++;
+ columnsJSON.forEach((c2) => {
+ const { key, label } = c2;
+
+ const objKey = label.split(" ").join("_");
+
+ reportValueMapping[objKey] = reportValue?.[key] ?? "";
+ });
+
+ return reportValueMapping;
+ });
+ const formData = new FormData();
+ formData.append("file", targetFiles);
+ formData.append("customFileName", `${formCode}.pdf`);
+ formData.append("reportDatas", JSON.stringify(reportDatas));
+ formData.append("reportTempPath", selectTemp);
+
+ const reqeustCreateReport = await fetch(
+ `${origin}/api/pdftron/createVendorDataReports`,
+ { method: "POST", body: formData }
+ );
+
+ if (reqeustCreateReport.ok) {
+ const blob = await reqeustCreateReport.blob();
+
+ saveAs(blob, `${formCode}.pdf`);
+
+ toastMessage.success("Report 다운로드 완료!")
+ } else {
+ const err = await reqeustCreateReport.json();
+ console.error("에러:", err);
+ throw new Error(err.message)
}
} catch (err) {
console.error(err);
toast({
title: "Error",
- description: "파일 업로드 중 오류가 발생했습니다.",
+ description: "Report 생성 중 오류가 발생했습니다.",
variant: "destructive",
});
} finally {
setIsUploading(false);
- setOpen(false);
+ setSelectedFiles([])
+ setOpen(false)
}
};
@@ -237,10 +265,10 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
<DialogFooter>
<Button
- disabled={selectedFiles.length === 0 || selectTemp.length === 0}
+ disabled={selectedFiles.length === 0 || selectTemp.length === 0 || isUploading}
onClick={submitData}
>
- 다운로드
+ {isUploading && <Loader2 />}다운로드
</Button>
</DialogFooter>
</DialogContent>
@@ -305,3 +333,17 @@ const updateReportTempList: UpdateReportTempList = async (
})
);
};
+
+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) : "";
+ }
+};
diff --git a/components/form-data/form-data-report-dialog.tsx b/components/form-data/form-data-report-dialog.tsx
index deb0873b..e28b4345 100644
--- a/components/form-data/form-data-report-dialog.tsx
+++ b/components/form-data/form-data-report-dialog.tsx
@@ -8,11 +8,10 @@ import React, {
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 { WebViewerInstance } from "@pdftron/webviewer";
+import { Loader2 } from "lucide-react";
+import { saveAs } from "file-saver";
+import { toast } from "sonner";
import {
Dialog,
DialogContent,
@@ -25,11 +24,11 @@ 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";
@@ -58,7 +57,9 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
setReportData,
packageId,
formId,
+ formCode,
}) => {
+
const [tempList, setTempList] = useState<tempFile[]>([]);
const [selectTemp, setSelectTemp] = useState<string>("");
const [instance, setInstance] = useState<null | WebViewerInstance>(null);
@@ -92,46 +93,9 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
// },
});
- const blob = new Blob([fileData], {
- type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- });
+ saveAs(new Blob([fileData]), fileName);
- 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);
- // }
+ toast.success("Report 다운로드 완료!");
}
};
@@ -175,6 +139,7 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
instance={instance}
setInstance={setInstance}
setFileLoading={setFileLoading}
+ formCode={formCode}
/>
</div>
@@ -195,6 +160,7 @@ interface ReportWebViewerProps {
instance: null | WebViewerInstance;
setInstance: Dispatch<SetStateAction<WebViewerInstance | null>>;
setFileLoading: Dispatch<SetStateAction<boolean>>;
+ formCode: string;
}
const ReportWebViewer: FC<ReportWebViewerProps> = ({
@@ -204,6 +170,7 @@ const ReportWebViewer: FC<ReportWebViewerProps> = ({
instance,
setInstance,
setFileLoading,
+ formCode,
}) => {
const [viwerLoading, setViewerLoading] = useState<boolean>(true);
const viewer = useRef<HTMLDivElement>(null);
@@ -234,12 +201,6 @@ const ReportWebViewer: FC<ReportWebViewerProps> = ({
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);
});
});
@@ -262,9 +223,10 @@ const ReportWebViewer: FC<ReportWebViewerProps> = ({
instance,
reportDatas,
reportTempPath,
- setFileLoading
+ setFileLoading,
+ formCode
);
- }, [reportTempPath, reportDatas, instance, columnsJSON]);
+ }, [reportTempPath, reportDatas, instance, columnsJSON, formCode]);
return (
<div ref={viewer} className="h-[100%]">
@@ -319,7 +281,8 @@ type ImportReportData = (
instance: null | WebViewerInstance,
reportDatas: ReportData[],
reportTempPath: string,
- setFileLoading: Dispatch<SetStateAction<boolean>>
+ setFileLoading: Dispatch<SetStateAction<boolean>>,
+ formCode: string
) => void;
const importReportData: ImportReportData = async (
@@ -327,7 +290,8 @@ const importReportData: ImportReportData = async (
instance,
reportDatas,
reportTempPath,
- setFileLoading
+ setFileLoading,
+ formCode
) => {
setFileLoading(true);
try {
@@ -352,12 +316,13 @@ const importReportData: ImportReportData = async (
});
const doc = await createDocument(reportFileBlob, {
+ filename: `${formCode}_report.docx`,
extension: "docx",
});
await doc.applyTemplateValues(reportValueMapping);
- documentViewer.loadDocument(doc, {
+ documentViewer.loadDocument(doc, {
extension: "docx",
enableOfficeEditing: true,
officeOptions: {
@@ -373,68 +338,6 @@ const importReportData: ImportReportData = async (
}
};
-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,
diff --git a/components/form-data/form-data-report-temp-upload-dialog.tsx b/components/form-data/form-data-report-temp-upload-dialog.tsx
index b646c3e6..69df704e 100644
--- a/components/form-data/form-data-report-temp-upload-dialog.tsx
+++ b/components/form-data/form-data-report-temp-upload-dialog.tsx
@@ -8,8 +8,11 @@ import React, {
useEffect,
} from "react";
import { useToast } from "@/hooks/use-toast";
+import { toast as toastMessage } from "sonner";
import prettyBytes from "pretty-bytes";
-import { X, Loader2 } from "lucide-react";
+import { X, Loader2, Download, Delete, Trash2 } from "lucide-react";
+import ExcelJS from "exceljs";
+import { saveAs } from "file-saver";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
@@ -40,10 +43,28 @@ import {
FileListItem,
FileListName,
} from "@/components/ui/file-list";
-import { getReportTempList, uploadReportTemp } from "@/lib/forms/services";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@/components/ui/alert-dialog";
+import {
+ getReportTempList,
+ uploadReportTemp,
+ getReportTempFileData,
+ deleteReportTempFile,
+} from "@/lib/forms/services";
import { VendorDataReportTemps } from "@/db/schema/vendorData";
+import { DataTableColumnJSON } from "./form-data-table-columns";
interface FormDataReportTempUploadDialogProps {
+ columnsJSON: DataTableColumnJSON[];
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
packageId: number;
@@ -57,7 +78,15 @@ const MAX_FILE_SIZE = 3000000;
export const FormDataReportTempUploadDialog: FC<
FormDataReportTempUploadDialogProps
-> = ({ open, setOpen, packageId, formId, uploaderType }) => {
+> = ({
+ columnsJSON,
+ open,
+ setOpen,
+ packageId,
+ formId,
+ formCode,
+ uploaderType,
+}) => {
const { toast } = useToast();
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [isUploading, setIsUploading] = useState(false);
@@ -131,55 +160,177 @@ export const FormDataReportTempUploadDialog: FC<
}
};
+ const downloadTempFile = async () => {
+ try {
+ const { fileName, fileType, base64 } = await getReportTempFileData();
+
+ saveAs(`data:${fileType};base64,${base64}`, fileName);
+
+ toastMessage.success("Report Sample File 다운로드 완료!");
+ } catch (err) {
+ console.log(err);
+ toast({
+ title: "Error",
+ description: "Sample File을 찾을 수가 없습니다.",
+ variant: "destructive",
+ });
+ }
+ };
+
+ const downloadReportVarList = async () => {
+ try {
+ // Create a new workbook
+ const workbook = new ExcelJS.Workbook();
+
+ // 데이터 시트 생성
+ const worksheet = workbook.addWorksheet("Data");
+
+ // 유효성 검사용 숨김 시트 생성
+ const validationSheet = workbook.addWorksheet("ValidationData");
+ validationSheet.state = "hidden"; // 시트 숨김 처리
+
+ // 1. 데이터 시트에 헤더 추가
+ const headers = ["Table Column Label", "Report Variable"];
+ worksheet.addRow(headers);
+
+ // 헤더 스타일 적용
+ const headerRow = worksheet.getRow(1);
+ headerRow.font = { bold: true };
+ headerRow.alignment = { horizontal: "center" };
+ headerRow.eachCell((cell) => {
+ cell.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFCCCCCC" },
+ };
+ });
+
+ // 2. 데이터 행 추가
+ columnsJSON.forEach((row) => {
+ const { displayLabel, label } = row;
+
+ const labelConvert = label.replaceAll(" ", "_");
+
+ worksheet.addRow([displayLabel, labelConvert]);
+ });
+
+ // 3. 컬럼 너비 자동 조정
+ headers.forEach((col, idx) => {
+ const column = worksheet.getColumn(idx + 1);
+
+ // 최적 너비 계산
+ let maxLength = col.length;
+ columnsJSON.forEach((row) => {
+ const valueKey = idx === 0 ? "displayLabel" : "label";
+
+ const value = row[valueKey];
+ if (value !== undefined && value !== null) {
+ const valueLength = String(value).length;
+ if (valueLength > maxLength) {
+ maxLength = valueLength;
+ }
+ }
+ });
+
+ // 너비 설정 (최소 10, 최대 50)
+ column.width = Math.min(Math.max(maxLength + 2, 10), 50);
+ });
+
+ const buffer = await workbook.xlsx.writeBuffer();
+ saveAs(new Blob([buffer]), `${formCode}_report_varible_list.xlsx`);
+ toastMessage.success("Report Varible List File 다운로드 완료!");
+ } catch (err) {
+ console.log(err);
+ toast({
+ title: "Error",
+ description: "Variable List 파일을 찾을 수가 없습니다.",
+ variant: "destructive",
+ });
+ }
+ };
+
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="w-[600px]" style={{ maxWidth: "none" }}>
<DialogHeader>
<DialogTitle>Report Template Upload</DialogTitle>
<DialogDescription>
- 사용하시고자 하는 Report Template를 업로드 하여주시기 바랍니다.
+ 사용하시고자 하는 Report Template(.docx)를 업로드 하여주시기
+ 바랍니다.
</DialogDescription>
</DialogHeader>
- {/* {prevReportTemp.length > 0 && (
- <>
- <Label>Prev Report Template</Label>
- <ScrollArea className="max-h-[100px]">
- {prevReportTemp.map((c, i) => {
- return <div key={i}>{i}</div>;
- })}
- </ScrollArea>
- </>
- )} */}
-
- <Dropzone
- maxSize={MAX_FILE_SIZE}
- multiple={true}
- accept={{ accept: [".docx"] }}
- onDropAccepted={handleDropAccepted}
- onDropRejected={handleDropRejected}
- disabled={isUploading}
- >
- {({ maxSize }) => (
- <>
- <DropzoneZone className="flex justify-center">
- <DropzoneInput />
- <div className="flex items-center gap-6">
- <DropzoneUploadIcon />
- <div className="grid gap-0.5">
- <DropzoneTitle>파일을 여기에 드롭하세요</DropzoneTitle>
- <DropzoneDescription>
- 또는 클릭하여 파일을 선택하세요. 최대 크기:{" "}
- {maxSize ? prettyBytes(maxSize) : "무제한"}
- </DropzoneDescription>
+ <div>
+ <Label>Sample Template Download</Label>
+
+ <FileList className="max-h-[200px] gap-3">
+ <FileListItem className="p-3">
+ <FileListHeader>
+ <FileListIcon />
+ <FileListInfo>
+ <FileListName>sample_template_file.docx</FileListName>
+ </FileListInfo>
+ <FileListAction onClick={downloadTempFile}>
+ <Download className="h-4 w-4" />
+ <span className="sr-only">Download</span>
+ </FileListAction>
+ </FileListHeader>
+ </FileListItem>
+ <FileListItem className="p-3">
+ <FileListHeader>
+ <FileListIcon />
+ <FileListInfo>
+ <FileListName>report_variable_list.xlsx</FileListName>
+ </FileListInfo>
+ <FileListAction onClick={downloadReportVarList}>
+ <Download className="h-4 w-4" />
+ <span className="sr-only">Download</span>
+ </FileListAction>
+ </FileListHeader>
+ </FileListItem>
+ </FileList>
+ </div>
+ <div>
+ <Label>Uploaded Template Files</Label>
+ <UploadedTempFiles
+ prevReportTemp={prevReportTemp}
+ updateReportTempList={() =>
+ updateReportTempList(packageId, formId, setPrevReportTemp)
+ }
+ />
+ </div>
+
+ <div>
+ <Label>Report Template File Upload(.docx)</Label>
+ <Dropzone
+ maxSize={MAX_FILE_SIZE}
+ multiple={true}
+ accept={{ accept: [".docx"] }}
+ onDropAccepted={handleDropAccepted}
+ onDropRejected={handleDropRejected}
+ disabled={isUploading}
+ >
+ {({ maxSize }) => (
+ <>
+ <DropzoneZone className="flex justify-center">
+ <DropzoneInput />
+ <div className="flex items-center gap-6">
+ <DropzoneUploadIcon />
+ <div className="grid gap-0.5">
+ <DropzoneTitle>파일을 여기에 드롭하세요</DropzoneTitle>
+ <DropzoneDescription>
+ 또는 클릭하여 파일을 선택하세요. 최대 크기:{" "}
+ {maxSize ? prettyBytes(maxSize) : "무제한"}
+ </DropzoneDescription>
+ </div>
</div>
- </div>
- </DropzoneZone>
- <Label className="text-xs text-muted-foreground">
- 여러 파일을 선택할 수 있습니다.
- </Label>
- </>
- )}
- </Dropzone>
+ </DropzoneZone>
+ <Label className="text-xs text-muted-foreground">
+ 여러 파일을 선택할 수 있습니다.
+ </Label>
+ </>
+ )}
+ </Dropzone>
+ </div>
{selectedFiles.length > 0 && (
<div className="grid gap-2">
@@ -223,7 +374,7 @@ const UploadFileItem: FC<UploadFileItemProps> = ({
isUploading,
}) => {
return (
- <FileList className="max-h-[200px] gap-3">
+ <FileList className="max-h-[100px] gap-3">
{selectedFiles.map((file, index) => (
<FileListItem key={index} className="p-3">
<FileListHeader>
@@ -303,3 +454,116 @@ const updateReportTempList: UpdateReportTempList = async (
const tempList = await getReportTempList(packageId, formId);
setPrevReportTemp(tempList);
};
+
+interface UploadedTempFiles {
+ prevReportTemp: VendorDataReportTemps[];
+ updateReportTempList: () => void;
+}
+
+const UploadedTempFiles: FC<UploadedTempFiles> = ({
+ prevReportTemp,
+ updateReportTempList,
+}) => {
+ const { toast } = useToast();
+
+ const downloadTempFile = async (fileName: string, filePath: string) => {
+ try {
+ const getTempFile = await fetch(filePath);
+
+ if (getTempFile.ok) {
+ const blob = await getTempFile.blob();
+
+ saveAs(blob, fileName);
+
+ toastMessage.success("Report 다운로드 완료!");
+ } else {
+ const err = await getTempFile.json();
+ console.error("에러:", err);
+ throw new Error(err.message);
+ }
+
+ toastMessage.success("Template File 다운로드 완료!");
+ } catch (err) {
+ console.error(err)
+ toast({
+ title: "Error",
+ description: "Template File 다운로드 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ }
+ };
+
+ const deleteTempFile = async (id: number) => {
+ try {
+ const { result, error } = await deleteReportTempFile(id);
+
+ if (result) {
+ updateReportTempList();
+ toastMessage.success("Template File 삭제 완료!");
+ } else {
+ throw new Error(error);
+ }
+ } catch (err) {
+ toast({
+ title: "Error",
+ description: "Template File 삭제 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ }
+ };
+
+ return (
+ <ScrollArea>
+ <FileList className="max-h-[100px] gap-3">
+ {prevReportTemp.map((c) => {
+ const { fileName, filePath, id } = c;
+
+ return (
+ <AlertDialog key={id}>
+ <FileListItem className="p-3">
+ <FileListHeader>
+ <FileListIcon />
+ <FileListInfo>
+ <FileListName>{fileName}</FileListName>
+ </FileListInfo>
+ <FileListAction
+ onClick={() => {
+ downloadTempFile(fileName, filePath);
+ }}
+ >
+ <Download className="h-4 w-4" />
+ <span className="sr-only">Download</span>
+ </FileListAction>
+ <AlertDialogTrigger asChild>
+ <FileListAction>
+ <Trash2 className="h-4 w-4" />
+ <span className="sr-only">Delete</span>
+ </FileListAction>
+ </AlertDialogTrigger>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>
+ Report Templete File({fileName})을 삭제하시겠습니까?
+ </AlertDialogTitle>
+ <AlertDialogDescription />
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel>취소</AlertDialogCancel>
+ <AlertDialogAction
+ onClick={() => {
+ deleteTempFile(id);
+ }}
+ >
+ 삭제
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </FileListHeader>
+ </FileListItem>
+ </AlertDialog>
+ );
+ })}
+ </FileList>
+ </ScrollArea>
+ );
+};
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx
index 9feaf3b2..823416c1 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -11,19 +11,22 @@ import {
DataTableColumnJSON,
ColumnType,
} from "./form-data-table-columns";
-
import type { DataTableAdvancedFilterField } from "@/types/table";
import { Button } from "../ui/button";
import { Download, Loader, Save, Upload } from "lucide-react";
import { toast } from "sonner";
import { syncMissingTags, updateFormDataInDB } from "@/lib/forms/services";
import { UpdateTagSheet } from "./update-form-sheet";
-
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
import { FormDataReportTempUploadDialog } from "./form-data-report-temp-upload-dialog";
import { FormDataReportDialog } from "./form-data-report-dialog";
import { FormDataReportBatchDialog } from "./form-data-report-batch-dialog";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
interface GenericData {
[key: string]: any;
@@ -526,20 +529,29 @@ export default function DynamicTable({
{/* 버튼 그룹 */}
<div className="flex items-center gap-2">
{/* 태그 불러오기 버튼 */}
- <Button
- variant="default"
- size="sm"
- onClick={() => setBatchDownDialog(true)}
- >
- Report Batch
- </Button>
- <Button
- variant="default"
- size="sm"
- onClick={() => setTempUpDialog(true)}
- >
- Temp Upload
- </Button>
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button variant="default" size="sm">
+ Report
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="flex flex-row gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setTempUpDialog(true)}
+ >
+ Template Upload
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setBatchDownDialog(true)}
+ >
+ Report Download
+ </Button>
+ </PopoverContent>
+ </Popover>
<Button
variant="default"
size="sm"
@@ -611,6 +623,7 @@ export default function DynamicTable({
/>
{tempUpDialog && (
<FormDataReportTempUploadDialog
+ columnsJSON={columnsJSON}
open={tempUpDialog}
setOpen={setTempUpDialog}
packageId={contractItemId}
@@ -645,5 +658,3 @@ export default function DynamicTable({
</>
);
}
-
-
diff --git a/config/poaColumnsConfig.ts b/config/poaColumnsConfig.ts
deleted file mode 100644
index 268a2259..00000000
--- a/config/poaColumnsConfig.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-import { POADetail } from "@/db/schema/contract"
-
-export interface PoaColumnConfig {
- id: keyof POADetail
- label: string
- group?: string
- excelHeader?: string
- type?: string
-}
-
-export const poaColumnsConfig: PoaColumnConfig[] = [
- {
- id: "id",
- label: "ID",
- excelHeader: "ID",
- group: "Key Info",
- type: "number",
- },
- {
- id: "projectId",
- label: "Project ID",
- excelHeader: "Project ID",
- group: "Key Info",
- type: "number",
- },
- {
- id: "vendorId",
- label: "Vendor ID",
- excelHeader: "Vendor ID",
- group: "Key Info",
- type: "number",
- },
- {
- id: "contractNo",
- label: "Form Code",
- excelHeader: "Form Code",
- group: "Original Info",
- type: "text",
- },
- {
- id: "originalContractName",
- label: "Contract Name",
- excelHeader: "Contract Name",
- group: "Original Info",
- type: "text",
- },
- {
- id: "originalStatus",
- label: "Status",
- excelHeader: "Status",
- group: "Original Info",
- type: "text",
- },
- {
- id: "deliveryTerms",
- label: "Delivery Terms",
- excelHeader: "Delivery Terms",
- group: "Change Info",
- type: "text",
- },
- {
- id: "deliveryDate",
- label: "Delivery Date",
- excelHeader: "Delivery Date",
- group: "Change Info",
- type: "date",
- },
- {
- id: "deliveryLocation",
- label: "Delivery Location",
- excelHeader: "Delivery Location",
- group: "Change Info",
- type: "text",
- },
- {
- id: "currency",
- label: "Currency",
- excelHeader: "Currency",
- group: "Change Info",
- type: "text",
- },
- {
- id: "totalAmount",
- label: "Total Amount",
- excelHeader: "Total Amount",
- group: "Change Info",
- type: "number",
- },
- {
- id: "discount",
- label: "Discount",
- excelHeader: "Discount",
- group: "Change Info",
- type: "number",
- },
- {
- id: "tax",
- label: "Tax",
- excelHeader: "Tax",
- group: "Change Info",
- type: "number",
- },
- {
- id: "shippingFee",
- label: "Shipping Fee",
- excelHeader: "Shipping Fee",
- group: "Change Info",
- type: "number",
- },
- {
- id: "netTotal",
- label: "Net Total",
- excelHeader: "Net Total",
- group: "Change Info",
- type: "number",
- },
- {
- id: "createdAt",
- label: "Created At",
- excelHeader: "Created At",
- group: "System Info",
- type: "date",
- },
- {
- id: "updatedAt",
- label: "Updated At",
- excelHeader: "Updated At",
- group: "System Info",
- type: "date",
- },
-] \ No newline at end of file
diff --git a/db/migrations/0097_poa_initial_setup.sql b/db/migrations/0097_poa_initial_setup.sql
deleted file mode 100644
index fae3f4d1..00000000
--- a/db/migrations/0097_poa_initial_setup.sql
+++ /dev/null
@@ -1,95 +0,0 @@
--- Drop existing tables and views
-DROP VIEW IF EXISTS change_orders_detail_view;
-DROP TABLE IF EXISTS change_order_items CASCADE;
-DROP TABLE IF EXISTS change_orders CASCADE;
-DROP VIEW IF EXISTS poa_detail_view;
-DROP TABLE IF EXISTS poa CASCADE;
-
--- Create POA table
-CREATE TABLE poa (
- id SERIAL PRIMARY KEY,
- contract_no VARCHAR(100) NOT NULL,
- original_contract_no VARCHAR(100) NOT NULL,
- project_id INTEGER NOT NULL,
- vendor_id INTEGER NOT NULL,
- original_contract_name VARCHAR(255) NOT NULL,
- original_status VARCHAR(50) NOT NULL,
- delivery_terms TEXT,
- delivery_date DATE,
- delivery_location VARCHAR(255),
- currency VARCHAR(10),
- total_amount NUMERIC(12,2),
- discount NUMERIC(12,2),
- tax NUMERIC(12,2),
- shipping_fee NUMERIC(12,2),
- net_total NUMERIC(12,2),
- change_reason TEXT,
- approval_status VARCHAR(50) DEFAULT 'PENDING',
- created_at TIMESTAMP NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
- CONSTRAINT poa_original_contract_no_contracts_contract_no_fk
- FOREIGN KEY (original_contract_no)
- REFERENCES contracts(contract_no)
- ON DELETE CASCADE,
- CONSTRAINT poa_project_id_projects_id_fk
- FOREIGN KEY (project_id)
- REFERENCES projects(id)
- ON DELETE CASCADE,
- CONSTRAINT poa_vendor_id_vendors_id_fk
- FOREIGN KEY (vendor_id)
- REFERENCES vendors(id)
- ON DELETE CASCADE
-);
-
--- Create POA detail view
-CREATE VIEW poa_detail_view AS
-SELECT
- -- POA primary information
- poa.id,
- poa.contract_no,
- poa.change_reason,
- poa.approval_status,
-
- -- Original PO information
- poa.original_contract_no,
- poa.original_contract_name,
- poa.original_status,
- c.start_date as original_start_date,
- c.end_date as original_end_date,
-
- -- Project information
- poa.project_id,
- p.code as project_code,
- p.name as project_name,
-
- -- Vendor information
- poa.vendor_id,
- v.vendor_name,
-
- -- Changed delivery details
- poa.delivery_terms,
- poa.delivery_date,
- poa.delivery_location,
-
- -- Changed financial information
- poa.currency,
- poa.total_amount,
- poa.discount,
- poa.tax,
- poa.shipping_fee,
- poa.net_total,
-
- -- Timestamps
- poa.created_at,
- poa.updated_at,
-
- -- Electronic signature status
- EXISTS (
- SELECT 1
- FROM contract_envelopes
- WHERE contract_envelopes.contract_id = poa.id
- ) as has_signature
-FROM poa
-LEFT JOIN contracts c ON poa.original_contract_no = c.contract_no
-LEFT JOIN projects p ON poa.project_id = p.id
-LEFT JOIN vendors v ON poa.vendor_id = v.id; \ No newline at end of file
diff --git a/db/schema/contract.ts b/db/schema/contract.ts
index c14921bb..10721b4d 100644
--- a/db/schema/contract.ts
+++ b/db/schema/contract.ts
@@ -257,106 +257,4 @@ export const contractsDetailView = pgView("contracts_detail_view").as((qb) => {
});
// Type inference for the view
-export type ContractDetail = typeof contractsDetailView.$inferSelect;
-
-
-
-
-// ============ poa (Purchase Order Amendment) ============
-export const poa = pgTable("poa", {
- // 주 키
- id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
-
- // Form code는 원본과 동일하게 유지
- contractNo: varchar("contract_no", { length: 100 }).notNull(),
-
- // 원본 PO 참조
- originalContractNo: varchar("original_contract_no", { length: 100 })
- .notNull()
- .references(() => contracts.contractNo, { onDelete: "cascade" }),
-
- // 원본 계약 정보
- projectId: integer("project_id")
- .notNull()
- .references(() => projects.id, { onDelete: "cascade" }),
- vendorId: integer("vendor_id")
- .notNull()
- .references(() => vendors.id, { onDelete: "cascade" }),
- originalContractName: varchar("original_contract_name", { length: 255 }).notNull(),
- originalStatus: varchar("original_status", { length: 50 }).notNull(),
-
- // 변경된 납품 조건
- deliveryTerms: text("delivery_terms"), // 변경된 납품 조건
- deliveryDate: date("delivery_date"), // 변경된 납품 기한
- deliveryLocation: varchar("delivery_location", { length: 255 }), // 변경된 납품 장소
-
- // 변경된 가격/금액 관련
- currency: varchar("currency", { length: 10 }), // 변경된 통화
- totalAmount: numeric("total_amount", { precision: 12, scale: 2 }), // 변경된 총 금액
- discount: numeric("discount", { precision: 12, scale: 2 }), // 변경된 할인
- tax: numeric("tax", { precision: 12, scale: 2 }), // 변경된 세금
- shippingFee: numeric("shipping_fee", { precision: 12, scale: 2 }), // 변경된 배송비
- netTotal: numeric("net_total", { precision: 12, scale: 2 }), // 변경된 순 총액
-
- // 변경 사유
- changeReason: text("change_reason"),
-
- // 승인 상태
- approvalStatus: varchar("approval_status", { length: 50 }).default("PENDING"),
-
- // 생성/수정 시각
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
-})
-
-// 타입 추론
-export type POA = typeof poa.$inferSelect
-
-// ============ poa_detail_view ============
-export const poaDetailView = pgView("poa_detail_view").as((qb) => {
- return qb
- .select({
- // POA primary information
- id: poa.id,
- contractNo: poa.contractNo,
- projectId: contracts.projectId,
- vendorId: contracts.vendorId,
- changeReason: poa.changeReason,
- approvalStatus: poa.approvalStatus,
-
- // Original PO information
- originalContractName: sql<string>`${contracts.contractName}`.as('original_contract_name'),
- originalStatus: sql<string>`${contracts.status}`.as('original_status'),
- originalStartDate: sql<Date>`${contracts.startDate}`.as('original_start_date'),
- originalEndDate: sql<Date>`${contracts.endDate}`.as('original_end_date'),
-
- // Changed delivery details
- deliveryTerms: poa.deliveryTerms,
- deliveryDate: poa.deliveryDate,
- deliveryLocation: poa.deliveryLocation,
-
- // Changed financial information
- currency: poa.currency,
- totalAmount: poa.totalAmount,
- discount: poa.discount,
- tax: poa.tax,
- shippingFee: poa.shippingFee,
- netTotal: poa.netTotal,
-
- // Timestamps
- createdAt: poa.createdAt,
- updatedAt: poa.updatedAt,
-
- // Electronic signature status
- hasSignature: sql<boolean>`EXISTS (
- SELECT 1
- FROM ${contractEnvelopes}
- WHERE ${contractEnvelopes.contractId} = ${poa.id}
- )`.as('has_signature'),
- })
- .from(poa)
- .leftJoin(contracts, eq(poa.contractNo, contracts.contractNo))
-});
-
-// Type inference for the view
-export type POADetail = typeof poaDetailView.$inferSelect; \ No newline at end of file
+export type ContractDetail = typeof contractsDetailView.$inferSelect; \ No newline at end of file
diff --git a/db/schema/vendorData.ts b/db/schema/vendorData.ts
index 92a92c8e..01a10b7e 100644
--- a/db/schema/vendorData.ts
+++ b/db/schema/vendorData.ts
@@ -3,32 +3,41 @@ import {
text,
varchar,
timestamp,
- integer, numeric, date, unique, serial, jsonb, uniqueIndex
-} from "drizzle-orm/pg-core"
-import { contractItems } from "./contract"
-
-export const forms = pgTable("forms", {
- id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
- contractItemId: integer("contract_item_id")
+ integer,
+ numeric,
+ date,
+ unique,
+ serial,
+ jsonb,
+ uniqueIndex,
+ } from "drizzle-orm/pg-core";
+ import { contractItems } from "./contract";
+
+ export const forms = pgTable(
+ "forms",
+ {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ contractItemId: integer("contract_item_id")
.notNull()
.references(() => contractItems.id, { onDelete: "cascade" }),
- formCode: varchar("form_code", { length: 100 }).notNull(),
- formName: varchar("form_name", { length: 255 }).notNull(),
- // tagType: varchar("tag_type", { length: 50 }).notNull(),
- // class: varchar("class", { length: 100 }).notNull(),
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
-}, (table) => {
- return {
+ formCode: varchar("form_code", { length: 100 }).notNull(),
+ formName: varchar("form_name", { length: 255 }).notNull(),
+ // tagType: varchar("tag_type", { length: 50 }).notNull(),
+ // class: varchar("class", { length: 100 }).notNull(),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+ },
+ (table) => {
+ return {
// contractItemId와 formCode의 조합을 유니크하게 설정
- contractItemFormCodeUnique: uniqueIndex("contract_item_form_code_unique").on(
- table.contractItemId,
- table.formCode
- ),
+ contractItemFormCodeUnique: uniqueIndex(
+ "contract_item_form_code_unique"
+ ).on(table.contractItemId, table.formCode),
+ };
}
-})
-
-export const rfqAttachments = pgTable("form_templates", {
+ );
+
+ export const rfqAttachments = pgTable("form_templates", {
id: serial("id").primaryKey(),
formId: integer("form_id").references(() => forms.id),
fileName: varchar("file_name", { length: 255 }).notNull(),
@@ -36,175 +45,204 @@ export const rfqAttachments = pgTable("form_templates", {
createdAt: timestamp("created_at").defaultNow().notNull(),
udpatedAt: timestamp("updated_at").defaultNow().notNull(),
+ });
-});
-
-
-export const formMetas = pgTable("form_metas", {
+ export const formMetas = pgTable("form_metas", {
id: serial("id").primaryKey(),
formCode: varchar("form_code", { length: 50 }).notNull(),
formName: varchar("form_name", { length: 255 }).notNull(),
columns: jsonb("columns").notNull(),
- createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-})
-
-export const formEntries = pgTable("form_entries", {
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ });
+
+ export const formEntries = pgTable("form_entries", {
id: serial("id").primaryKey(),
formCode: varchar("form_code", { length: 50 }).notNull(),
data: jsonb("data").notNull(),
contractItemId: integer("contract_item_id")
- .notNull()
- .references(() => contractItems.id, { onDelete: "cascade" }),
- createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-})
-
-
-// ============ tags (각 계약 아이템에 대한 Tag) ============
-// "어느 계약의 어느 아이템에 대한 태그"임을 나타내려면 contract_items를 참조
-export const tags = pgTable("tags", {
+ .notNull()
+ .references(() => contractItems.id, { onDelete: "cascade" }),
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ });
+
+ // ============ tags (각 계약 아이템에 대한 Tag) ============
+ // "어느 계약의 어느 아이템에 대한 태그"임을 나타내려면 contract_items를 참조
+ export const tags = pgTable("tags", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
-
+
// 이 Tag가 속한 "계약 내 아이템" (즉 contract_items.id)
contractItemId: integer("contract_item_id")
- .notNull()
- .references(() => contractItems.id, { onDelete: "cascade" }),
-
- formId: integer("form_id")
- .references(() => forms.id, { onDelete: "set null" }),
-
+ .notNull()
+ .references(() => contractItems.id, { onDelete: "cascade" }),
+
+ formId: integer("form_id").references(() => forms.id, {
+ onDelete: "set null",
+ }),
+
tagNo: varchar("tag_no", { length: 100 }).notNull(),
tagType: varchar("tag_type", { length: 50 }).notNull(),
class: varchar("class", { length: 100 }).notNull(),
description: text("description"),
-
+
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
-})
-
-export type Tag = typeof tags.$inferSelect
-export type Form = typeof forms.$inferSelect
-export type NewTag = typeof tags.$inferInsert
-
-export const tagTypes = pgTable("tag_types", {
+ });
+
+ export type Tag = typeof tags.$inferSelect;
+ export type Form = typeof forms.$inferSelect;
+ export type NewTag = typeof tags.$inferInsert;
+
+ export const tagTypes = pgTable("tag_types", {
code: varchar("code", { length: 50 }).primaryKey(),
description: text("description").notNull(),
-
- createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-})
-
-export const tagSubfields = pgTable("tag_subfields", {
- id: serial("id").primaryKey(),
-
- // 외래키: tagTypeCode -> tagTypes.code
- tagTypeCode: varchar("tag_type_code", { length: 50 })
+
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ });
+
+ export const tagSubfields = pgTable(
+ "tag_subfields",
+ {
+ id: serial("id").primaryKey(),
+
+ // 외래키: tagTypeCode -> tagTypes.code
+ tagTypeCode: varchar("tag_type_code", { length: 50 })
.notNull()
.references(() => tagTypes.code, { onDelete: "cascade" }),
-
- /**
- * 나머지 필드
- */
- // tagTypeDescription: -> 이제 불필요. tagTypes.description로 join
- attributesId: varchar("attributes_id", { length: 50 }).notNull(),
- attributesDescription: text("attributes_description").notNull(),
-
- expression: text("expression"),
- delimiter: varchar("delimiter", { length: 10 }),
-
- sortOrder: integer("sort_order").default(0).notNull(),
-
- createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-}, (table) => {
- return {
+
+ /**
+ * 나머지 필드
+ */
+ // tagTypeDescription: -> 이제 불필요. tagTypes.description로 join
+ attributesId: varchar("attributes_id", { length: 50 }).notNull(),
+ attributesDescription: text("attributes_description").notNull(),
+
+ expression: text("expression"),
+ delimiter: varchar("delimiter", { length: 10 }),
+
+ sortOrder: integer("sort_order").default(0).notNull(),
+
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ },
+ (table) => {
+ return {
uniqTagTypeAttribute: unique("uniq_tag_type_attribute").on(
- table.tagTypeCode,
- table.attributesId
+ table.tagTypeCode,
+ table.attributesId
),
- };
-});
-
-export const tagSubfieldOptions = pgTable("tag_subfield_options", {
+ };
+ }
+ );
+
+ export const tagSubfieldOptions = pgTable("tag_subfield_options", {
id: serial("id").primaryKey(),
-
+
// 어떤 subfield에 속하는 옵션인지
attributesId: varchar("attributes_id", { length: 50 })
- .notNull()
- .references(() => tagSubfields.attributesId, { onDelete: "cascade" }),
-
+ .notNull()
+ .references(() => tagSubfields.attributesId, { onDelete: "cascade" }),
+
/**
* 실제 코드 (예: "PM", "AA", "VB", "VAR", "01", "02" ...)
*/
code: varchar("code", { length: 50 }).notNull(),
-
+
/**
* 사용자에게 보여줄 레이블 (예: "Pump", "Pneumatic Motor", "Ball Valve", ...)
*/
label: text("label").notNull(),
-
+
/**
* 생성/수정 시각 (선택)
*/
- createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-})
-
-export const tagClasses = pgTable("tag_classes", {
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ });
+
+ export const tagClasses = pgTable("tag_classes", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
-
+
// 기존 code/label
code: varchar("code", { length: 100 }).notNull(),
label: text("label").notNull(),
-
+
// 새 필드: tagTypeCode -> references tagTypes.code
tagTypeCode: varchar("tag_type_code", { length: 50 })
- .notNull()
- .references(() => tagTypes.code, { onDelete: "cascade" }),
-
+ .notNull()
+ .references(() => tagTypes.code, { onDelete: "cascade" }),
+
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
-})
-
-export const tagTypeClassFormMappings = pgTable("tag_type_class_form_mappings", {
- id: serial("id").primaryKey(),
-
- tagTypeLabel: varchar("tag_type_label", { length: 255 }).notNull(),
- classLabel: varchar("class_label", { length: 255 }).notNull(),
-
- formCode: varchar("form_code", { length: 50 }).notNull(),
- formName: varchar("form_name", { length: 255 }).notNull(),
-
- createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-})
-
-export type TagTypeClassFormMappings = typeof tagTypeClassFormMappings.$inferSelect
-export type TagSubfields = typeof tagSubfields.$inferSelect
-export type TagSubfieldOption = typeof tagSubfieldOptions.$inferSelect
-export type TagClasses = typeof tagClasses.$inferSelect
-
-
-export const viewTagSubfields = pgTable("view_tag_subfields", {
+ });
+
+ export const tagTypeClassFormMappings = pgTable(
+ "tag_type_class_form_mappings",
+ {
+ id: serial("id").primaryKey(),
+
+ tagTypeLabel: varchar("tag_type_label", { length: 255 }).notNull(),
+ classLabel: varchar("class_label", { length: 255 }).notNull(),
+
+ formCode: varchar("form_code", { length: 50 }).notNull(),
+ formName: varchar("form_name", { length: 255 }).notNull(),
+
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ }
+ );
+
+ export type TagTypeClassFormMappings =
+ typeof tagTypeClassFormMappings.$inferSelect;
+ export type TagSubfields = typeof tagSubfields.$inferSelect;
+ export type TagSubfieldOption = typeof tagSubfieldOptions.$inferSelect;
+ export type TagClasses = typeof tagClasses.$inferSelect;
+
+ export const viewTagSubfields = pgTable("view_tag_subfields", {
id: integer("id").primaryKey(),
-
+
tagTypeCode: varchar("tag_type_code", { length: 50 }).notNull(),
tagTypeDescription: text("tag_type_description"),
attributesId: varchar("attributes_id", { length: 50 }).notNull(),
attributesDescription: text("attributes_description").notNull(),
-
+
expression: text("expression"),
delimiter: varchar("delimiter", { length: 10 }),
sortOrder: integer("sort_order").default(0).notNull(),
-
+
createdAt: timestamp("created_at", { withTimezone: true }),
updatedAt: timestamp("updated_at", { withTimezone: true }),
-})
-
-export type ViewTagSubfields = typeof viewTagSubfields.$inferSelect
-
-export const vendorDataReportTemps = pgTable("vendor_data_report_temps", {
+ });
+
+ export type ViewTagSubfields = typeof viewTagSubfields.$inferSelect;
+
+ export const vendorDataReportTemps = pgTable("vendor_data_report_temps", {
id: serial("id").primaryKey(),
contractItemId: integer("contract_item_id")
.notNull()
@@ -215,11 +253,12 @@ export const vendorDataReportTemps = pgTable("vendor_data_report_temps", {
fileName: varchar("file_name", { length: 255 }).notNull(),
filePath: varchar("file_path", { length: 1024 }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
- .defaultNow()
- .notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true })
- .defaultNow()
- .notNull(),
+ .defaultNow()
+ .notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
});
- export type VendorDataReportTemps = typeof vendorDataReportTemps.$inferSelect; \ No newline at end of file
+ export type VendorDataReportTemps = typeof vendorDataReportTemps.$inferSelect;
+
diff --git a/db/seeds_2/poaSeed.ts b/db/seeds_2/poaSeed.ts
deleted file mode 100644
index d93cde14..00000000
--- a/db/seeds_2/poaSeed.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { faker } from "@faker-js/faker"
-import db from "../db"
-import { contracts, poa } from "../schema/contract"
-import { sql } from "drizzle-orm"
-
-export async function seedPOA({ count = 10 } = {}) {
- try {
- console.log(`📝 Inserting POA ${count}`)
-
- // 기존 POA 데이터 삭제 및 ID 시퀀스 초기화
- await db.delete(poa)
- await db.execute(sql`ALTER SEQUENCE poa_id_seq RESTART WITH 1;`)
- console.log("✅ 기존 POA 데이터 삭제 및 ID 초기화 완료")
-
- // 조선업 맥락에 맞는 예시 문구들
- const deliveryTermsExamples = [
- "FOB 부산항",
- "CIF 상하이항",
- "DAP 울산조선소",
- "DDP 거제 옥포조선소",
- ]
- const deliveryLocations = [
- "부산 영도조선소",
- "울산 본사 도크 #3",
- "거제 옥포조선소 해양공장",
- "목포신항 부두",
- ]
- const changeReasonExamples = [
- "납품 일정 조정 필요",
- "자재 사양 변경",
- "선박 설계 변경에 따른 수정",
- "추가 부품 요청",
- "납품 장소 변경",
- "계약 조건 재협상"
- ]
-
- // 1. 기존 계약(PO) 목록 가져오기
- const existingContracts = await db.select().from(contracts)
- console.log(`Found ${existingContracts.length} existing contracts`)
-
- if (existingContracts.length === 0) {
- throw new Error("계약(PO) 데이터가 없습니다. 먼저 계약 데이터를 생성해주세요.")
- }
-
- // 2. POA 생성
- for (let i = 0; i < count; i++) {
- try {
- // 랜덤으로 원본 계약 선택
- const originalContract = faker.helpers.arrayElement(existingContracts)
- console.log(`Selected original contract: ${originalContract.contractNo}`)
-
- // POA 생성
- const totalAmount = faker.number.float({ min: 5000000, max: 500000000 })
- const discount = faker.helpers.maybe(() => faker.number.float({ min: 0, max: 500000 }), { probability: 0.3 })
- const tax = faker.helpers.maybe(() => faker.number.float({ min: 0, max: 1000000 }), { probability: 0.8 })
- const shippingFee = faker.helpers.maybe(() => faker.number.float({ min: 0, max: 300000 }), { probability: 0.5 })
- const netTotal = totalAmount - (discount || 0) + (tax || 0) + (shippingFee || 0)
-
- const poaData = {
- // Form code는 원본과 동일하게 유지
- contractNo: originalContract.contractNo,
- originalContractNo: originalContract.contractNo,
- projectId: originalContract.projectId,
- vendorId: originalContract.vendorId,
- originalContractName: originalContract.contractName,
- originalStatus: originalContract.status,
-
- // 변경 가능한 정보들
- deliveryTerms: faker.helpers.arrayElement(deliveryTermsExamples),
- deliveryDate: faker.helpers.maybe(() => faker.date.future().toISOString(), { probability: 0.7 }),
- deliveryLocation: faker.helpers.arrayElement(deliveryLocations),
- currency: "KRW",
- totalAmount: totalAmount.toString(),
- discount: discount?.toString(),
- tax: tax?.toString(),
- shippingFee: shippingFee?.toString(),
- netTotal: netTotal.toString(),
- changeReason: faker.helpers.arrayElement(changeReasonExamples),
- approvalStatus: faker.helpers.arrayElement(["PENDING", "APPROVED", "REJECTED"]),
- createdAt: new Date(),
- updatedAt: new Date(),
- }
-
- console.log("POA data:", poaData)
-
- await db.insert(poa).values(poaData)
- console.log(`Created POA for contract: ${originalContract.contractNo}`)
- } catch (error) {
- console.error(`Error creating POA ${i + 1}:`, error)
- throw error
- }
- }
-
- console.log(`✅ Successfully added ${count} new POAs`)
- } catch (error) {
- console.error("Error in seedPOA:", error)
- throw error
- }
-}
-
-// 실행
-if (require.main === module) {
- seedPOA({ count: 5 })
- .then(() => process.exit(0))
- .catch((error) => {
- console.error(error)
- process.exit(1)
- })
-} \ No newline at end of file
diff --git a/lib/forms/services.ts b/lib/forms/services.ts
index ff21626c..22f10466 100644
--- a/lib/forms/services.ts
+++ b/lib/forms/services.ts
@@ -1,6 +1,7 @@
// lib/forms/services.ts
"use server";
+import { headers } from "next/headers";
import path from "path";
import fs from "fs/promises";
import { v4 as uuidv4 } from "uuid";
@@ -805,3 +806,63 @@ export async function uploadReportTemp(
});
}
}
+
+export const getOrigin = async (): Promise<string> => {
+ const headersList = await headers();
+ const host = headersList.get("host");
+ const proto = headersList.get("x-forwarded-proto") || "http"; // 기본값은 http
+ const origin = `${proto}://${host}`;
+
+ return origin;
+};
+
+export const getReportTempFileData = async (): Promise<{
+ fileName: string;
+ fileType: string;
+ base64: string;
+}> => {
+ const fileName = "sample_template_file.docx";
+
+ const tempFile = await fs.readFile(
+ `public/vendorFormReportSample/${fileName}`
+ );
+
+ return {
+ fileName,
+ fileType:
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ base64: tempFile.toString("base64"),
+ };
+};
+
+type deleteReportTempFile = (id: number) => Promise<{
+ result: boolean;
+ error?: any;
+}>;
+
+export const deleteReportTempFile: deleteReportTempFile = async (id) => {
+ try {
+ return db.transaction(async (tx) => {
+ const [targetTempFile] = await tx
+ .select()
+ .from(vendorDataReportTemps)
+ .where(eq(vendorDataReportTemps.id, id));
+
+ if (!targetTempFile) {
+ throw new Error("해당 Template File을 찾을 수 없습니다.");
+ }
+
+ await tx
+ .delete(vendorDataReportTemps)
+ .where(eq(vendorDataReportTemps.id, id));
+
+ const { filePath } = targetTempFile;
+
+ await fs.unlink("public" + filePath);
+
+ return { result: true };
+ });
+ } catch (err) {
+ return { result: false, error: (err as Error).message };
+ }
+};
diff --git a/lib/pdftron/serverSDK/createReport.ts b/lib/pdftron/serverSDK/createReport.ts
new file mode 100644
index 00000000..412ada87
--- /dev/null
+++ b/lib/pdftron/serverSDK/createReport.ts
@@ -0,0 +1,83 @@
+const { PDFNet } = require("@pdftron/pdfnet-node");
+
+type CreateReport = (
+ coverPage: Buffer,
+ reportTempPath: string,
+ reportDatas: {
+ [key: string]: any;
+ }[]
+) => Promise<{
+ result: boolean;
+ buffer?: ArrayBuffer;
+ error?: any;
+}>;
+
+export const createReport: CreateReport = async (
+ coverPage,
+ reportTempPath,
+ reportDatas
+) => {
+ const main = async () => {
+ await PDFNet.initialize(process.env.NEXT_PUBLIC_PDFTRON_SERVER_KEY);
+
+ const mainDoc = await PDFNet.PDFDoc.create();
+ const buf = await PDFNet.Convert.office2PDFBuffer(coverPage);
+ const coverPDFDoc = await PDFNet.PDFDoc.createFromBuffer(buf);
+
+ const options = new PDFNet.Convert.OfficeToPDFOptions();
+
+ await mainDoc.insertPages(
+ (await mainDoc.getPageCount()) + 1,
+ coverPDFDoc,
+ 1,
+ await coverPDFDoc.getPageCount(),
+ PDFNet.PDFDoc.InsertFlag.e_none
+ );
+
+ for (const reportData of reportDatas) {
+ const resportDataJson = JSON.stringify(reportData);
+
+ const templateDoc = await PDFNet.Convert.createOfficeTemplateWithPath(
+ "public" + reportTempPath,
+ options
+ );
+
+ const pdfdoc = await templateDoc.fillTemplateJson(resportDataJson);
+
+ await mainDoc.insertPages(
+ (await mainDoc.getPageCount()) + 1,
+ pdfdoc,
+ 1,
+ await pdfdoc.getPageCount(),
+ PDFNet.PDFDoc.InsertFlag.e_none
+ );
+ }
+
+ // await mainDoc.save("test1.pdf", PDFNet.SDFDoc.SaveOptions.e_linearized);
+
+ const buffer = await mainDoc.saveMemoryBuffer(
+ PDFNet.SDFDoc.SaveOptions.e_linearized
+ );
+
+ return {
+ result: true,
+ buffer,
+ };
+ };
+
+ const result = await PDFNet.runWithCleanup(
+ main,
+ process.env.NEXT_PUBLIC_PDFTRON_SERVER_KEY
+ )
+ .catch((err: any) => {
+ return {
+ result: false,
+ error: err,
+ };
+ })
+ .then(async (data: any) => {
+ return data;
+ });
+
+ return result;
+};
diff --git a/lib/po/service.ts b/lib/po/service.ts
index f697bd58..5f2e4f35 100644
--- a/lib/po/service.ts
+++ b/lib/po/service.ts
@@ -324,7 +324,6 @@ Remarks:${contract.remarks}`,
const { success: sendDocuSignResult, envelopeId } = sendDocuSign;
-
await tx
.update(contracts)
.set({
@@ -344,7 +343,8 @@ Remarks:${contract.remarks}`,
const fileName = `${contractNo}-signature.pdf`;
const ext = path.extname(fileName);
const uniqueName = uuidv4() + ext;
- // Create a single envelope for all signers
+
+ // Create a single envelope for all signers
const [newEnvelope] = await tx
.insert(contractEnvelopes)
.values({
diff --git a/lib/poa/service.ts b/lib/poa/service.ts
deleted file mode 100644
index a11cbdd8..00000000
--- a/lib/poa/service.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-"use server";
-
-import db from "@/db/db";
-import { GetChangeOrderSchema } from "./validations";
-import { unstable_cache } from "@/lib/unstable-cache";
-import { filterColumns } from "@/lib/filter-columns";
-import {
- asc,
- desc,
- ilike,
- and,
- or,
- count,
-} from "drizzle-orm";
-
-import {
- poaDetailView,
-} from "@/db/schema/contract";
-
-/**
- * POA 목록 조회
- */
-export async function getChangeOrders(input: GetChangeOrderSchema) {
- return unstable_cache(
- async () => {
- try {
- const offset = (input.page - 1) * input.perPage;
-
- // 1. Build where clause
- let advancedWhere;
- try {
- advancedWhere = filterColumns({
- table: poaDetailView,
- filters: input.filters,
- joinOperator: input.joinOperator,
- });
- } catch (whereErr) {
- console.error("Error building advanced where:", whereErr);
- advancedWhere = undefined;
- }
-
- let globalWhere;
- if (input.search) {
- try {
- const s = `%${input.search}%`;
- globalWhere = or(
- ilike(poaDetailView.contractNo, s),
- ilike(poaDetailView.originalContractName, s),
- ilike(poaDetailView.projectCode, s),
- ilike(poaDetailView.projectName, s),
- ilike(poaDetailView.vendorName, s)
- );
- } catch (searchErr) {
- console.error("Error building search where:", searchErr);
- globalWhere = undefined;
- }
- }
-
- // 2. Combine where clauses
- let finalWhere;
- if (advancedWhere && globalWhere) {
- finalWhere = and(advancedWhere, globalWhere);
- } else {
- finalWhere = advancedWhere || globalWhere;
- }
-
- // 3. Build order by
- let orderBy;
- try {
- orderBy =
- input.sort.length > 0
- ? input.sort.map((item) =>
- item.desc
- ? desc(poaDetailView[item.id])
- : asc(poaDetailView[item.id])
- )
- : [desc(poaDetailView.createdAt)];
- } catch (orderErr) {
- console.error("Error building order by:", orderErr);
- orderBy = [desc(poaDetailView.createdAt)];
- }
-
- // 4. Execute queries
- let data = [];
- let total = 0;
-
- try {
- const queryBuilder = db.select().from(poaDetailView);
-
- if (finalWhere) {
- queryBuilder.where(finalWhere);
- }
-
- queryBuilder.orderBy(...orderBy);
- queryBuilder.offset(offset).limit(input.perPage);
-
- data = await queryBuilder;
-
- const countBuilder = db
- .select({ count: count() })
- .from(poaDetailView);
-
- if (finalWhere) {
- countBuilder.where(finalWhere);
- }
-
- const countResult = await countBuilder;
- total = countResult[0]?.count || 0;
- } catch (queryErr) {
- console.error("Query execution failed:", queryErr);
- throw queryErr;
- }
-
- const pageCount = Math.ceil(total / input.perPage);
-
- return { data, pageCount };
- } catch (err) {
- console.error("Error in getChangeOrders:", err);
- if (err instanceof Error) {
- console.error("Error message:", err.message);
- console.error("Error stack:", err.stack);
- }
- return { data: [], pageCount: 0 };
- }
- },
- [JSON.stringify(input)],
- {
- revalidate: 3600,
- tags: [`poa`],
- }
- )();
-} \ No newline at end of file
diff --git a/lib/poa/table/poa-table-columns.tsx b/lib/poa/table/poa-table-columns.tsx
deleted file mode 100644
index b362e54c..00000000
--- a/lib/poa/table/poa-table-columns.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type DataTableRowAction } from "@/types/table"
-import { type ColumnDef } from "@tanstack/react-table"
-import { InfoIcon, PenIcon } from "lucide-react"
-
-import { formatDate } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "@/components/ui/tooltip"
-import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
-import { POADetail } from "@/db/schema/contract"
-import { poaColumnsConfig } from "@/config/poaColumnsConfig"
-
-interface GetColumnsProps {
- setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<POADetail> | null>>
-}
-
-/**
- * tanstack table column definitions with nested headers
- */
-export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<POADetail>[] {
- // ----------------------------------------------------------------
- // 1) actions column (buttons for item info)
- // ----------------------------------------------------------------
- const actionsColumn: ColumnDef<POADetail> = {
- id: "actions",
- enableHiding: false,
- cell: function Cell({ row }) {
- const hasSignature = row.original.hasSignature;
-
- return (
- <div className="flex items-center space-x-1">
- {/* Item Info Button */}
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="ghost"
- size="icon"
- onClick={() => setRowAction({ row, type: "items" })}
- >
- <InfoIcon className="h-4 w-4" aria-hidden="true" />
- </Button>
- </TooltipTrigger>
- <TooltipContent>
- View Item Info
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
-
- {/* Signature Request Button - only show if no signature exists */}
- {!hasSignature && (
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="ghost"
- size="icon"
- onClick={() => setRowAction({ row, type: "signature" })}
- >
- <PenIcon className="h-4 w-4" aria-hidden="true" />
- </Button>
- </TooltipTrigger>
- <TooltipContent>
- Request Electronic Signature
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- )}
- </div>
- );
- },
- size: 80,
- };
-
- // ----------------------------------------------------------------
- // 2) Regular columns grouped by group name
- // ----------------------------------------------------------------
- // 2-1) groupMap: { [groupName]: ColumnDef<POADetail>[] }
- const groupMap: Record<string, ColumnDef<POADetail>[]> = {};
-
- poaColumnsConfig.forEach((cfg) => {
- // Use "_noGroup" if no group is specified
- const groupName = cfg.group || "_noGroup";
-
- if (!groupMap[groupName]) {
- groupMap[groupName] = [];
- }
-
- // Child column definition
- const childCol: ColumnDef<POADetail> = {
- accessorKey: cfg.id,
- enableResizing: true,
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={cfg.label} />
- ),
- meta: {
- excelHeader: cfg.excelHeader,
- group: cfg.group,
- type: cfg.type,
- },
- cell: ({ cell }) => {
- const value = cell.getValue();
-
- if (cfg.type === "date") {
- const dateVal = value as Date;
- return (
- <div className="text-sm">
- {formatDate(dateVal)}
- </div>
- );
- }
- if (cfg.type === "number") {
- const numVal = value as number;
- return (
- <div className="text-sm">
- {numVal ? numVal.toLocaleString() : "-"}
- </div>
- );
- }
- return (
- <div className="text-sm">
- {value ?? "-"}
- </div>
- );
- },
- };
-
- groupMap[groupName].push(childCol);
- });
-
- // ----------------------------------------------------------------
- // 2-2) Create actual parent columns (groups) from the groupMap
- // ----------------------------------------------------------------
- const nestedColumns: ColumnDef<POADetail>[] = [];
-
- // Order can be fixed by pre-defining group order or sorting
- Object.entries(groupMap).forEach(([groupName, colDefs]) => {
- if (groupName === "_noGroup") {
- // No group → Add as top-level columns
- nestedColumns.push(...colDefs);
- } else {
- // Parent column
- nestedColumns.push({
- id: groupName,
- header: groupName,
- columns: colDefs,
- });
- }
- });
-
- // ----------------------------------------------------------------
- // 3) Final column array: nestedColumns + actionsColumn
- // ----------------------------------------------------------------
- return [
- ...nestedColumns,
- actionsColumn,
- ];
-} \ No newline at end of file
diff --git a/lib/poa/table/poa-table-toolbar-actions.tsx b/lib/poa/table/poa-table-toolbar-actions.tsx
deleted file mode 100644
index 97a9cc55..00000000
--- a/lib/poa/table/poa-table-toolbar-actions.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-import { Download, RefreshCcw } from "lucide-react"
-
-import { exportTableToExcel } from "@/lib/export"
-import { Button } from "@/components/ui/button"
-import { POADetail } from "@/db/schema/contract"
-
-interface ItemsTableToolbarActionsProps {
- table: Table<POADetail>
-}
-
-export function PoaTableToolbarActions({ table }: ItemsTableToolbarActionsProps) {
- return (
- <div className="flex items-center gap-2">
- {/** Refresh 버튼 */}
- <Button
- variant="samsung"
- size="sm"
- className="gap-2"
- >
- <RefreshCcw className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">Get POAs</span>
- </Button>
-
- {/** Export 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={() =>
- exportTableToExcel(table, {
- filename: "poa-list",
- excludeColumns: ["select", "actions"],
- })
- }
- className="gap-2"
- >
- <Download className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">Export</span>
- </Button>
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/poa/table/poa-table.tsx b/lib/poa/table/poa-table.tsx
deleted file mode 100644
index a5cad02a..00000000
--- a/lib/poa/table/poa-table.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-"use client"
-
-import * as React from "react"
-import type {
- DataTableAdvancedFilterField,
- DataTableFilterField,
- DataTableRowAction,
-} from "@/types/table"
-
-import { useDataTable } from "@/hooks/use-data-table"
-import { DataTable } from "@/components/data-table/data-table"
-import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
-
-import { getChangeOrders } from "../service"
-import { POADetail } from "@/db/schema/contract"
-import { getColumns } from "./poa-table-columns"
-import { PoaTableToolbarActions } from "./poa-table-toolbar-actions"
-
-interface ItemsTableProps {
- promises: Promise<
- [
- Awaited<ReturnType<typeof getChangeOrders>>,
- ]
- >
-}
-
-export function ChangeOrderListsTable({ promises }: ItemsTableProps) {
- const [result] = React.use(promises)
- const { data, pageCount } = result
-
- const [rowAction, setRowAction] =
- React.useState<DataTableRowAction<POADetail> | null>(null)
-
- // Handle row actions
- React.useEffect(() => {
- if (!rowAction) return
-
- if (rowAction.type === "items") {
- // Handle items view action
- setRowAction(null)
- }
- }, [rowAction])
-
- const columns = React.useMemo(
- () => getColumns({ setRowAction }),
- [setRowAction]
- )
-
- const filterFields: DataTableFilterField<POADetail>[] = [
- {
- id: "contractNo",
- label: "계약번호",
- },
- {
- id: "originalContractName",
- label: "계약명",
- },
- {
- id: "approvalStatus",
- label: "승인 상태",
- },
- ]
-
- const advancedFilterFields: DataTableAdvancedFilterField<POADetail>[] = [
- {
- id: "contractNo",
- label: "계약번호",
- type: "text",
- },
- {
- id: "originalContractName",
- label: "계약명",
- type: "text",
- },
- {
- id: "projectId",
- label: "프로젝트 ID",
- type: "number",
- },
- {
- id: "vendorId",
- label: "벤더 ID",
- type: "number",
- },
- {
- id: "originalStatus",
- label: "상태",
- type: "text",
- },
- {
- id: "deliveryTerms",
- label: "납품조건",
- type: "text",
- },
- {
- id: "deliveryDate",
- label: "납품기한",
- type: "date",
- },
- {
- id: "deliveryLocation",
- label: "납품장소",
- type: "text",
- },
- {
- id: "currency",
- label: "통화",
- type: "text",
- },
- {
- id: "totalAmount",
- label: "총 금액",
- type: "number",
- },
- {
- id: "discount",
- label: "할인",
- type: "number",
- },
- {
- id: "tax",
- label: "세금",
- type: "number",
- },
- {
- id: "shippingFee",
- label: "배송비",
- type: "number",
- },
- {
- id: "netTotal",
- label: "최종 금액",
- type: "number",
- },
- {
- id: "changeReason",
- label: "변경 사유",
- type: "text",
- },
- {
- id: "approvalStatus",
- label: "승인 상태",
- type: "text",
- },
- {
- id: "createdAt",
- label: "생성일",
- type: "date",
- },
- {
- id: "updatedAt",
- label: "수정일",
- type: "date",
- },
- ]
-
- const { table } = useDataTable({
- data,
- columns,
- pageCount,
- filterFields,
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState: {
- sorting: [{ id: "createdAt", desc: true }],
- columnPinning: { right: ["actions"] },
- },
- getRowId: (originalRow) => String(originalRow.id),
- shallow: false,
- clearOnDefault: true,
- })
-
- return (
- <>
- <DataTable
- table={table}
- className="h-[calc(100vh-12rem)]"
- >
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- shallow={false}
- >
- <PoaTableToolbarActions table={table} />
- </DataTableAdvancedToolbar>
- </DataTable>
- </>
- )
-} \ No newline at end of file
diff --git a/lib/poa/validations.ts b/lib/poa/validations.ts
deleted file mode 100644
index eae1b5ab..00000000
--- a/lib/poa/validations.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import {
- createSearchParamsCache,
- parseAsArrayOf,
- parseAsInteger,
- parseAsString,
- parseAsStringEnum,
-} from "nuqs/server"
-import * as z from "zod"
-
-import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"
-import { POADetail } from "@/db/schema/contract"
-
-export const searchParamsCache = createSearchParamsCache({
- // UI 모드나 플래그 관련
- flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]),
-
- // 페이징
- page: parseAsInteger.withDefault(1),
- perPage: parseAsInteger.withDefault(10),
-
- // 정렬 (createdAt 기준 내림차순)
- sort: getSortingStateParser<POADetail>().withDefault([
- { id: "createdAt", desc: true },
- ]),
-
- // 원본 PO 관련
- contractNo: parseAsString.withDefault(""),
- originalContractName: parseAsString.withDefault(""),
- originalStatus: parseAsString.withDefault(""),
- originalStartDate: parseAsString.withDefault(""),
- originalEndDate: parseAsString.withDefault(""),
-
- // 프로젝트 정보
- projectId: parseAsString.withDefault(""),
- projectCode: parseAsString.withDefault(""),
- projectName: parseAsString.withDefault(""),
-
- // 벤더 정보
- vendorId: parseAsString.withDefault(""),
- vendorName: parseAsString.withDefault(""),
-
- // 납품 관련
- deliveryTerms: parseAsString.withDefault(""),
- deliveryDate: parseAsString.withDefault(""),
- deliveryLocation: parseAsString.withDefault(""),
-
- // 금액 관련
- currency: parseAsString.withDefault(""),
- totalAmount: parseAsString.withDefault(""),
- discount: parseAsString.withDefault(""),
- tax: parseAsString.withDefault(""),
- shippingFee: parseAsString.withDefault(""),
- netTotal: parseAsString.withDefault(""),
-
- // 변경 사유 및 승인 상태
- changeReason: parseAsString.withDefault(""),
- approvalStatus: parseAsString.withDefault(""),
-
- // 고급 필터(Advanced) & 검색
- filters: getFiltersStateParser().withDefault([]),
- joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
- search: parseAsString.withDefault(""),
-})
-
-// 최종 타입
-export type GetChangeOrderSchema = Awaited<ReturnType<typeof searchParamsCache.parse>> \ No newline at end of file
diff --git a/pages/api/pdftron/createVendorDataReports.ts b/pages/api/pdftron/createVendorDataReports.ts
index 47f6055d..f461a7fb 100644
--- a/pages/api/pdftron/createVendorDataReports.ts
+++ b/pages/api/pdftron/createVendorDataReports.ts
@@ -1,5 +1,8 @@
import type { NextApiRequest, NextApiResponse } from "next";
+import type { File as FormidableFile } from "formidable";
import formidable from "formidable";
+import fs from "fs/promises";
+import { createReport } from "@/lib/pdftron/serverSDK/createReport";
export const config = {
api: {
@@ -15,22 +18,61 @@ export default async function handler(
return res.status(405).end();
}
- const form = formidable({ multiples: true });
-
- form.parse(req, async (err, fields, files) => {
- if (err) {
- console.error(err);
- return res.status(500).json({ error: "Error parsing form" });
- }
-
- try {
- const additionalData = JSON.parse((fields?.additionalData ?? "") as string);
- console.log("📦 additionalData:", additionalData);
- console.log("📎 files:", files.files); // files.files는 array or single file
-
- return res.status(200).json({ success: true });
- } catch (e) {
- return res.status(400).json({ error: "Invalid additionalData" });
- }
- });
+ try {
+ const form = formidable({ multiples: false });
+
+ form.parse(req, async (err, fields, files) => {
+ if (err) {
+ console.error(err);
+ return res.status(500).json({ error: "Error parsing form" });
+ }
+
+ try {
+ const fileName = fields?.customFileName?.[0] ?? "";
+ const reportDatas = JSON.parse(fields?.reportDatas?.[0] ?? "[]") as {
+ [key: string]: any;
+ }[];
+ const reportTempPath = fields?.reportTempPath?.[0] ?? "";
+ const reportCoverPage: FormidableFile | undefined = files?.file?.[0];
+
+ if (
+ !reportCoverPage ||
+ fileName.length === 0 ||
+ reportDatas.length === 0 ||
+ reportTempPath.length === 0
+ ) {
+ return res.status(400).json({ error: "Invalid Report Data" });
+ }
+
+ const buffer = await fs.readFile(reportCoverPage.filepath);
+
+ const {
+ result,
+ buffer: pdfBuffer,
+ error,
+ } = await createReport(buffer, reportTempPath, reportDatas);
+
+ if (result && pdfBuffer) {
+ res.setHeader("Content-Type", "application/pdf");
+ res.setHeader(
+ "Content-Disposition",
+ `attachment; filename="${fileName}"`
+ );
+
+ return res.send(Buffer.from(pdfBuffer));
+ }
+
+ return res.status(200).json({
+ success: false,
+ message: "Report 생성에 실패하였습니다.",
+ error,
+ });
+ } catch (e) {
+ console.log(e);
+ return res.status(400).json({ error: "Invalid additionalData" });
+ }
+ });
+ } catch (err) {
+ return res.status(401).end();
+ }
}
diff --git a/pages/api/po/sendDocuSign.ts b/pages/api/po/sendDocuSign.ts
index ccb83733..eb218c2c 100644
--- a/pages/api/po/sendDocuSign.ts
+++ b/pages/api/po/sendDocuSign.ts
@@ -5,7 +5,7 @@ export const config = {
};
import type { NextApiRequest, NextApiResponse } from "next";
-import { requestContractSign } from "@/lib/docuSign/docuSignFns";
+import { requestContractSign, getRecipients } from "@/lib/docuSign/docuSignFns";
export default async function handler(
req: NextApiRequest,
@@ -36,11 +36,13 @@ export default async function handler(
const { result, envelopeId, error } = docuSignStart;
- res.status(200).json({
+ const sendResult = {
success: result,
envelopeId,
message: error?.message,
- });
+ };
+
+ res.status(200).json(sendResult);
} catch (error: any) {
res
.status(500)
diff --git a/pages/api/po/webhook.ts b/pages/api/po/webhook.ts
index e61246a2..39a6931e 100644
--- a/pages/api/po/webhook.ts
+++ b/pages/api/po/webhook.ts
@@ -3,15 +3,11 @@ export const config = {
bodyParser: true, // ✅ 이게 false면 안 됨!
},
};
-
import type { NextApiRequest, NextApiResponse } from "next";
import path from "path";
import fs from "fs";
import * as z from "zod";
import db from "@/db/db";
-import { GetPOSchema } from "@/lib/po/validations";
-import { unstable_cache } from "@/lib/unstable-cache";
-import { filterColumns } from "@/lib/filter-columns";
import {
asc,
desc,
@@ -25,18 +21,15 @@ import {
eq,
count,
} from "drizzle-orm";
-import { countPos, selectPos } from "@/lib/po/repository";
import {
contractEnvelopes,
- contractsDetailView,
contractSigners,
contracts,
} from "@/db/schema/contract";
-import { vendors, vendorContacts } from "@/db/schema/vendors";
-import dayjs from "dayjs";
-
-import { POContent } from "@/lib/docuSign/types";
-import { downloadContractFile, getRecipients } from "@/lib/docuSign/docuSignFns";
+import {
+ downloadContractFile,
+ getRecipients,
+} from "@/lib/docuSign/docuSignFns";
export default async function handler(
req: NextApiRequest,
@@ -108,7 +101,7 @@ export default async function handler(
await tx
.update(contracts)
- .set({ status: `$FAILED_${safeRole}_SEND_MAIL(${message})` })
+ .set({ status: `FAILED_${safeRole}_SEND_MAIL(${message})` })
.where(eq(contracts.id, contractId));
await tx
@@ -154,6 +147,10 @@ export default async function handler(
.where(eq(dbSchma.envelopeId, envelopeId))
.limit(1);
+ if (!targetContract) {
+ continue;
+ }
+
const { id, contractId } = targetContract;
if (contractId === null || contractId === undefined) {
@@ -210,6 +207,10 @@ export default async function handler(
.where(eq(dbSchma.envelopeId, envelopeId))
.limit(1);
+ if (!targetContract) {
+ continue;
+ }
+
const { id, contractId, fileName, filePath } = targetContract;
if (contractId === null || contractId === undefined) {
@@ -277,6 +278,10 @@ export default async function handler(
.where(eq(dbSchma.envelopeId, envelopeId))
.limit(1);
+ if (!targetContract) {
+ continue;
+ }
+
const { contractId, fileName, filePath } = targetContract;
if (contractId === null || contractId === undefined) {
diff --git a/public/vendorFormReportSample/sample_template_file.docx b/public/vendorFormReportSample/sample_template_file.docx
new file mode 100644
index 00000000..0e338eb8
--- /dev/null
+++ b/public/vendorFormReportSample/sample_template_file.docx
Binary files differ