summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/pq/helper.ts22
-rw-r--r--lib/pq/pq-review-table-new/pq-filter-sheet.tsx247
-rw-r--r--lib/pq/pq-review-table-new/user-combobox.tsx2
-rw-r--r--lib/pq/pq-review-table-new/vendors-table-columns.tsx114
-rw-r--r--lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx26
-rw-r--r--lib/pq/pq-review-table-new/vendors-table.tsx17
-rw-r--r--lib/pq/service.ts80
-rw-r--r--lib/site-visit/client-site-visit-wrapper.tsx6
-rw-r--r--lib/site-visit/service.ts48
-rw-r--r--lib/vendor-data-plant/services copy.ts99
-rw-r--r--lib/vendor-investigation/table/investigation-table-columns.tsx7
-rw-r--r--lib/vendor-regular-registrations/service.ts6
-rw-r--r--lib/vendors/table/request-pq-dialog.tsx25
-rw-r--r--lib/vendors/table/vendors-table.tsx1
14 files changed, 542 insertions, 158 deletions
diff --git a/lib/pq/helper.ts b/lib/pq/helper.ts
index 81ee5db2..d3970fd7 100644
--- a/lib/pq/helper.ts
+++ b/lib/pq/helper.ts
@@ -1,11 +1,12 @@
-import {
- vendorPQSubmissions,
- vendors,
- projects,
- users,
- vendorInvestigations
+import {
+ vendorPQSubmissions,
+ vendors,
+ projects,
+ users,
+ vendorInvestigations
} from "@/db/schema"
import { CustomColumnMapping } from "../filter-columns"
+import { sql } from "drizzle-orm"
/**
* Helper function to create custom column mapping for PQ submissions
@@ -41,6 +42,12 @@ export function createPQFilterMapping(): CustomColumnMapping {
investigationStatus: { table: vendorInvestigations, column: "investigationStatus" },
investigationAddress: { table: vendorInvestigations, column: "investigationAddress" },
qmManagerId: { table: vendorInvestigations, column: "qmManagerId" },
+
+ // pqItems JSON 검색 (첫 번째 아이템의 itemCode 또는 itemName)
+ pqItems: {
+ table: vendorPQSubmissions,
+ column: "pqItems"
+ },
}
}
@@ -90,5 +97,8 @@ export function createPQDirectColumnMapping(): CustomColumnMapping {
investigationStatus: vendorInvestigations.investigationStatus,
investigationAddress: vendorInvestigations.investigationAddress,
qmManagerId: vendorInvestigations.qmManagerId,
+
+ // pqItems JSON 검색 (첫 번째 아이템의 itemCode 또는 itemName)
+ pqItems: vendorPQSubmissions.pqItems,
}
} \ No newline at end of file
diff --git a/lib/pq/pq-review-table-new/pq-filter-sheet.tsx b/lib/pq/pq-review-table-new/pq-filter-sheet.tsx
index ff1b890b..fe525a46 100644
--- a/lib/pq/pq-review-table-new/pq-filter-sheet.tsx
+++ b/lib/pq/pq-review-table-new/pq-filter-sheet.tsx
@@ -7,7 +7,7 @@ import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { CalendarIcon, ChevronRight, Search, X } from "lucide-react"
import { customAlphabet } from "nanoid"
-import { parseAsStringEnum, useQueryState } from "nuqs"
+import { parseAsStringEnum, useQueryState, parseAsString } from "nuqs"
import { Button } from "@/components/ui/button"
import {
@@ -40,8 +40,11 @@ const pqFilterSchema = z.object({
requesterName: z.string().optional(),
pqNumber: z.string().optional(),
vendorName: z.string().optional(),
+ vendorCode: z.string().optional(),
+ type: z.string().optional(),
+ projectName: z.string().optional(),
status: z.string().optional(),
- evaluationResult: z.string().optional(),
+ pqItems: z.string().optional(),
createdAtRange: z.object({
from: z.date().optional(),
to: z.date().optional(),
@@ -57,13 +60,13 @@ const pqStatusOptions = [
{ value: "REJECTED", label: "거부됨" },
]
-// 평가 결과 옵션 정의
-const evaluationResultOptions = [
- { value: "APPROVED", label: "승인" },
- { value: "SUPPLEMENT", label: "보완" },
- { value: "REJECTED", label: "불가" },
+// PQ 유형 옵션 정의
+const pqTypeOptions = [
+ { value: "GENERAL", label: "일반 PQ" },
+ { value: "PROJECT", label: "프로젝트 PQ" },
]
+
type PQFilterFormValues = z.infer<typeof pqFilterSchema>
interface PQFilterSheetProps {
@@ -106,6 +109,10 @@ export function PQFilterSheet({
// 현재 URL의 페이지 파라미터도 가져옴
const [page, setPage] = useQueryState("page", { defaultValue: "1" })
+ // DateRangePicker의 from/to 파라미터 직접 읽기
+ const [fromParam, setFromParam] = useQueryState("from", parseAsString.withDefault(""))
+ const [toParam, setToParam] = useQueryState("to", parseAsString.withDefault(""))
+
// 폼 상태 초기화
const form = useForm<PQFilterFormValues>({
resolver: zodResolver(pqFilterSchema),
@@ -113,8 +120,11 @@ export function PQFilterSheet({
requesterName: "",
pqNumber: "",
vendorName: "",
+ vendorCode: "",
+ type: "",
+ projectName: "",
status: "",
- evaluationResult: "",
+ pqItems: "",
createdAtRange: {
from: undefined,
to: undefined,
@@ -128,12 +138,22 @@ export function PQFilterSheet({
const currentFiltersString = JSON.stringify(filters);
// 패널이 열렸고, 필터가 있고, 마지막에 적용된 필터와 다를 때만 업데이트
- if (isOpen && filters && filters.length > 0 && currentFiltersString !== lastAppliedFilters.current) {
+ if (isOpen && (filters && filters.length > 0 || fromParam || toParam) && currentFiltersString !== lastAppliedFilters.current) {
setIsInitializing(true);
const formValues = { ...form.getValues() };
let formUpdated = false;
+ // DateRangePicker의 from/to 파라미터 처리
+ if (fromParam || toParam) {
+ formValues.createdAtRange = {
+ from: fromParam ? new Date(fromParam) : undefined,
+ to: toParam ? new Date(toParam) : undefined,
+ };
+ formUpdated = true;
+ }
+
+ // 기존 필터 처리
filters.forEach(filter => {
if (filter.id === "createdAt" && Array.isArray(filter.value) && filter.value.length > 0) {
formValues.createdAtRange = {
@@ -156,7 +176,7 @@ export function PQFilterSheet({
setIsInitializing(false);
}
- }, [filters, isOpen])
+ }, [filters, fromParam, toParam, isOpen])
// 현재 적용된 필터 카운트
const getActiveFilterCount = () => {
@@ -171,7 +191,13 @@ async function onSubmit(data: PQFilterFormValues) {
startTransition(async () => {
try {
// 필터 배열 생성
- const newFilters = []
+ const newFilters: Array<{
+ id: string;
+ value: string | string[];
+ type: string;
+ operator: string;
+ rowId: string;
+ }> = []
if (data.requesterName?.trim()) {
newFilters.push({
@@ -203,6 +229,36 @@ async function onSubmit(data: PQFilterFormValues) {
})
}
+ if (data.vendorCode?.trim()) {
+ newFilters.push({
+ id: "vendorCode",
+ value: data.vendorCode.trim(),
+ type: "text",
+ operator: "iLike",
+ rowId: generateId()
+ })
+ }
+
+ if (data.type?.trim()) {
+ newFilters.push({
+ id: "type",
+ value: data.type.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.projectName?.trim()) {
+ newFilters.push({
+ id: "projectName",
+ value: data.projectName.trim(),
+ type: "text",
+ operator: "iLike",
+ rowId: generateId()
+ })
+ }
+
if (data.status?.trim()) {
newFilters.push({
id: "status",
@@ -213,24 +269,24 @@ async function onSubmit(data: PQFilterFormValues) {
})
}
- if (data.evaluationResult?.trim()) {
+ if (data.pqItems?.trim()) {
newFilters.push({
- id: "evaluationResult",
- value: data.evaluationResult.trim(),
- type: "select",
- operator: "eq",
+ id: "pqItems",
+ value: data.pqItems.trim(),
+ type: "text",
+ operator: "iLike",
rowId: generateId()
})
}
- // 생성일 범위 추가
+ // PQ 전송일 범위 추가 (createdAt)
if (data.createdAtRange?.from) {
newFilters.push({
id: "createdAt",
value: [
data.createdAtRange.from.toISOString().split('T')[0],
- data.createdAtRange.to ? data.createdAtRange.to.toISOString().split('T')[0] : undefined
- ].filter(Boolean),
+ data.createdAtRange.to?.toISOString().split('T')[0]
+ ].filter((v): v is string => v !== undefined),
type: "date",
operator: "isBetween",
rowId: generateId()
@@ -289,8 +345,11 @@ async function onSubmit(data: PQFilterFormValues) {
requesterName: "",
pqNumber: "",
vendorName: "",
+ vendorCode: "",
+ type: "",
+ projectName: "",
status: "",
- evaluationResult: "",
+ pqItems: "",
createdAtRange: { from: undefined, to: undefined },
});
@@ -373,11 +432,11 @@ async function onSubmit(data: PQFilterFormValues) {
name="requesterName"
render={({ field }) => (
<FormItem>
- <FormLabel>요청자명</FormLabel>
+ <FormLabel>PQ/실사 요청자</FormLabel>
<FormControl>
<div className="relative">
<Input
- placeholder="요청자명 입력"
+ placeholder="PQ/실사 요청자 입력"
{...field}
className={cn(field.value && "pr-8", "bg-white")}
disabled={isInitializing}
@@ -411,11 +470,11 @@ async function onSubmit(data: PQFilterFormValues) {
name="pqNumber"
render={({ field }) => (
<FormItem>
- <FormLabel>PQ 번호</FormLabel>
+ <FormLabel>PQ No.</FormLabel>
<FormControl>
<div className="relative">
<Input
- placeholder="PQ 번호 입력"
+ placeholder="PQ No. 입력"
{...field}
className={cn(field.value && "pr-8", "bg-white")}
disabled={isInitializing}
@@ -449,7 +508,7 @@ async function onSubmit(data: PQFilterFormValues) {
name="vendorName"
render={({ field }) => (
<FormItem>
- <FormLabel>협력업체명</FormLabel>
+ <FormLabel>협력업체</FormLabel>
<FormControl>
<div className="relative">
<Input
@@ -482,7 +541,7 @@ async function onSubmit(data: PQFilterFormValues) {
/>
{/* PQ 상태 */}
- <FormField
+ {/* <FormField
control={form.control}
name="status"
render={({ field }) => (
@@ -527,15 +586,15 @@ async function onSubmit(data: PQFilterFormValues) {
<FormMessage />
</FormItem>
)}
- />
+ /> */}
- {/* 평가 결과 */}
+ {/* PQ 유형 */}
<FormField
control={form.control}
- name="evaluationResult"
+ name="type"
render={({ field }) => (
<FormItem>
- <FormLabel>평가 결과</FormLabel>
+ <FormLabel>PQ 유형</FormLabel>
<Select
value={field.value}
onValueChange={field.onChange}
@@ -544,7 +603,7 @@ async function onSubmit(data: PQFilterFormValues) {
<FormControl>
<SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
<div className="flex justify-between w-full">
- <SelectValue placeholder="평가 결과 선택" />
+ <SelectValue placeholder="PQ 유형 선택" />
{field.value && (
<Button
type="button"
@@ -553,7 +612,7 @@ async function onSubmit(data: PQFilterFormValues) {
className="h-4 w-4 -mr-2"
onClick={(e) => {
e.stopPropagation();
- form.setValue("evaluationResult", "");
+ form.setValue("type", "");
}}
disabled={isInitializing}
>
@@ -565,7 +624,7 @@ async function onSubmit(data: PQFilterFormValues) {
</SelectTrigger>
</FormControl>
<SelectContent>
- {evaluationResultOptions.map(option => (
+ {pqTypeOptions.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -577,13 +636,127 @@ async function onSubmit(data: PQFilterFormValues) {
)}
/>
- {/* PQ 생성일 */}
+ {/* 프로젝트명 */}
+ {/* <FormField
+ control={form.control}
+ name="projectName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>프로젝트명</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder="프로젝트명 입력"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("projectName", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ <span className="sr-only">Clear</span>
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ /> */}
+
+ {/* 협력업체 코드 */}
+ <FormField
+ control={form.control}
+ name="vendorCode"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>협력업체 코드</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder="협력업체 코드 입력"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("vendorCode", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ <span className="sr-only">Clear</span>
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 실사품목 */}
+ <FormField
+ control={form.control}
+ name="pqItems"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>실사품목</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder="실사품목 입력"
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("pqItems", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ <span className="sr-only">Clear</span>
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* PQ 전송일 */}
<FormField
control={form.control}
name="createdAtRange"
render={({ field }) => (
<FormItem>
- <FormLabel>PQ 생성일</FormLabel>
+ <FormLabel>PQ 전송일 범위</FormLabel>
<FormControl>
<div className="relative">
<DateRangePicker
@@ -591,8 +764,8 @@ async function onSubmit(data: PQFilterFormValues) {
triggerClassName="w-full bg-white"
align="start"
showClearButton={true}
- placeholder="PQ 생성일 범위를 선택하세요"
- value={field.value || undefined}
+ placeholder="PQ 전송일 범위를 선택하세요"
+ defaultDateRange={field.value?.from ? field.value : undefined}
onChange={field.onChange}
disabled={isInitializing}
/>
diff --git a/lib/pq/pq-review-table-new/user-combobox.tsx b/lib/pq/pq-review-table-new/user-combobox.tsx
index 560f675a..3e0264ed 100644
--- a/lib/pq/pq-review-table-new/user-combobox.tsx
+++ b/lib/pq/pq-review-table-new/user-combobox.tsx
@@ -87,7 +87,7 @@ export function UserCombobox({
{users.map((user) => (
<CommandItem
key={user.id}
- value={user.email} // 이메일을 value로 사용
+ value={`${user.name} ${user.email}`} // 이메일 및 이름을 value로 사용
onSelect={() => {
onChange(user.id)
setOpen(false)
diff --git a/lib/pq/pq-review-table-new/vendors-table-columns.tsx b/lib/pq/pq-review-table-new/vendors-table-columns.tsx
index e3687f52..ffa15e56 100644
--- a/lib/pq/pq-review-table-new/vendors-table-columns.tsx
+++ b/lib/pq/pq-review-table-new/vendors-table-columns.tsx
@@ -122,25 +122,54 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef
// ----------------------------------------------------------------
const selectColumn: ColumnDef<PQSubmission> = {
id: "select",
- header: ({ table }) => (
- <Checkbox
- checked={
- table.getIsAllPageRowsSelected() ||
- (table.getIsSomePageRowsSelected() && "indeterminate")
- }
- onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
- aria-label="Select all"
- className="translate-y-0.5"
- />
- ),
- cell: ({ row }) => (
- <Checkbox
- checked={row.getIsSelected()}
- onCheckedChange={(value) => row.toggleSelected(!!value)}
- aria-label="Select row"
- className="translate-y-0.5"
- />
- ),
+ header: ({ table }) => {
+ const selectedRows = table.getSelectedRowModel().rows;
+ const isAllSelected = table.getIsAllPageRowsSelected();
+ const isSomeSelected = table.getIsSomePageRowsSelected();
+
+ return (
+ <Checkbox
+ checked={isAllSelected}
+ indeterminate={isSomeSelected && !isAllSelected}
+ onCheckedChange={(value) => {
+ if (value) {
+ // 전체 선택: 첫 번째 행만 선택
+ table.toggleAllRowsSelected(false);
+ if (table.getRowModel().rows.length > 0) {
+ table.getRowModel().rows[0].toggleSelected(true);
+ }
+ } else {
+ // 전체 해제
+ table.toggleAllRowsSelected(false);
+ }
+ }}
+ aria-label="Select all"
+ className="translate-y-0.5"
+ />
+ );
+ },
+ cell: ({ row, table }) => {
+ const selectedRows = table.getSelectedRowModel().rows;
+ const isCurrentlySelected = row.getIsSelected();
+
+ return (
+ <Checkbox
+ checked={isCurrentlySelected}
+ onCheckedChange={(value) => {
+ if (value) {
+ // 체크하려는 경우: 이미 선택된 행이 있으면 모두 해제하고 현재 행만 선택
+ table.toggleAllRowsSelected(false);
+ row.toggleSelected(true);
+ } else {
+ // 체크 해제하는 경우
+ row.toggleSelected(false);
+ }
+ }}
+ aria-label="Select row"
+ className="translate-y-0.5"
+ />
+ );
+ },
size: 40,
enableSorting: false,
enableHiding: false,
@@ -172,7 +201,7 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef
cell: ({ row }) => (
<div className="flex flex-col">
<span className="font-medium">{row.getValue("vendorName")}</span>
- <span className="text-xs text-muted-foreground">{row.original.vendorCode ? row.original.vendorCode : "-"}/{row.original.taxId}</span>
+ {/* <span className="text-xs text-muted-foreground">{row.original.vendorCode ? row.original.vendorCode : "-"}/{row.original.taxId}</span> */}
</div>
),
enableSorting: true,
@@ -408,6 +437,26 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef
)
},
}
+ const investigationRequestedAtColumn: ColumnDef<PQSubmission> = {
+ accessorKey: "investigationRequestedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="실사 의뢰일" />
+ ),
+ cell: ({ row }) => {
+ const investigation = row.original.investigation;
+
+ if (!investigation || !investigation.requestedAt) {
+ return <span className="text-muted-foreground">-</span>;
+ }
+ const dateVal = investigation.requestedAt
+
+ return (
+ <div className="flex items-center gap-2">
+ <span>{dateVal ? formatDate(dateVal, 'KR') : "-"}</span>
+ </div>
+ )
+ },
+ }
const investigationNotesColumn: ColumnDef<PQSubmission> = {
accessorKey: "investigationNotes",
@@ -484,26 +533,7 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef
}
- const investigationRequestedAtColumn: ColumnDef<PQSubmission> = {
- accessorKey: "investigationRequestedAt",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="실사 의뢰일" />
- ),
- cell: ({ row }) => {
- const investigation = row.original.investigation;
-
- if (!investigation || !investigation.requestedAt) {
- return <span className="text-muted-foreground">-</span>;
- }
- const dateVal = investigation.requestedAt
- return (
- <div className="flex items-center gap-2">
- <span>{dateVal ? formatDate(dateVal, 'KR') : "-"}</span>
- </div>
- )
- },
- }
const investigationForecastedAtColumn: ColumnDef<PQSubmission> = {
@@ -651,9 +681,9 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef
)}
</DropdownMenuItem>
- {/* 방문실사 버튼 - 제품검사평가 또는 방문실사평가인 경우에만 표시 */}
- {pq.investigation &&
- (pq.investigation.investigationMethod === "PRODUCT_INSPECTION" ||
+ {/* 방문실사 버튼 - PQ가 승인됨 상태이고 제품검사평가 또는 방문실사평가인 경우에만 표시 */}
+ {pq.status === "APPROVED" && pq.investigation &&
+ (pq.investigation.investigationMethod === "PRODUCT_INSPECTION" ||
pq.investigation.investigationMethod === "SITE_VISIT_EVAL") && (
<>
<DropdownMenuItem
diff --git a/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx b/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx
index a5185cab..ea6b6189 100644
--- a/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx
+++ b/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx
@@ -624,7 +624,11 @@ const handleOpenRequestDialog = async () => {
variant="outline"
size="sm"
onClick={() => setIsCancelDialogOpen(true)}
- disabled={isLoading || selectedRows.length === 0}
+ disabled={
+ isLoading ||
+ selectedRows.length === 0 ||
+ !selectedRows.every(row => row.original.investigation?.investigationStatus === "PLANNED")
+ }
className="gap-2"
>
<X className="size-4" aria-hidden="true" />
@@ -636,7 +640,11 @@ const handleOpenRequestDialog = async () => {
variant="outline"
size="sm"
onClick={() => setIsReRequestDialogOpen(true)}
- disabled={isLoading || selectedRows.length === 0}
+ disabled={
+ isLoading ||
+ selectedRows.length === 0 ||
+ !selectedRows.every(row => row.original.investigation?.investigationStatus === "CANCELED")
+ }
className="gap-2"
>
<RefreshCw className="size-4" aria-hidden="true" />
@@ -648,7 +656,19 @@ const handleOpenRequestDialog = async () => {
variant="outline"
size="sm"
onClick={() => setIsSendResultsDialogOpen(true)}
- disabled={isLoading || selectedRows.length === 0}
+ disabled={
+ isLoading ||
+ selectedRows.length === 0 ||
+ !selectedRows.every(row => {
+ const investigation = row.original.investigation;
+ if (!investigation) return false;
+
+ // 실사 완료 상태이거나 평가 결과가 있는 경우에만 활성화
+ return investigation.investigationStatus === "COMPLETED" ||
+ investigation.evaluationResult === "REJECTED" ||
+ investigation.evaluationResult === "SUPPLEMENT";
+ })
+ }
className="gap-2"
>
<Send className="size-4" aria-hidden="true" />
diff --git a/lib/pq/pq-review-table-new/vendors-table.tsx b/lib/pq/pq-review-table-new/vendors-table.tsx
index 191c8bfa..f8f9928e 100644
--- a/lib/pq/pq-review-table-new/vendors-table.tsx
+++ b/lib/pq/pq-review-table-new/vendors-table.tsx
@@ -265,8 +265,9 @@ export function PQSubmissionsTable({ promises, className }: PQSubmissionsTablePr
{ label: "승인됨", value: "APPROVED" },
{ label: "거부됨", value: "REJECTED" },
]},
+ { id: "pqItems", label: "실사품목", type: "text" },
- { id: "createdAt", label: "생성일", type: "date" },
+ { id: "createdAt", label: "PQ 전송일", type: "date" },
{ id: "submittedAt", label: "제출일", type: "date" },
{ id: "approvedAt", label: "승인일", type: "date" },
{ id: "rejectedAt", label: "거부일", type: "date" },
@@ -286,8 +287,18 @@ export function PQSubmissionsTable({ promises, className }: PQSubmissionsTablePr
}),
columnVisibility: currentSettings.columnVisibility,
columnPinning: currentSettings.pinnedColumns,
+ // DateRangePicker의 from/to 파라미터를 위한 상태
+ columnFilters: initialSettings.from || initialSettings.to ? [
+ {
+ id: "createdAt",
+ value: {
+ from: initialSettings.from ? new Date(initialSettings.from) : undefined,
+ to: initialSettings.to ? new Date(initialSettings.to) : undefined,
+ }
+ }
+ ] : [],
}
- }, [currentSettings, initialSettings.sort, columns])
+ }, [currentSettings, initialSettings.sort, initialSettings.from, initialSettings.to, columns])
const { table } = useDataTable({
data: tableData.data,
@@ -297,6 +308,8 @@ export function PQSubmissionsTable({ promises, className }: PQSubmissionsTablePr
filterFields, // RFQ와 달리 빈 배열이 아닌 실제 필터 필드 사용
enablePinning: true,
enableAdvancedFilter: true,
+ enableRowSelection: true,
+ maxSelections: 1,
initialState,
getRowId: (originalRow) => String(originalRow.id),
shallow: false,
diff --git a/lib/pq/service.ts b/lib/pq/service.ts
index f58a1d4d..7296b836 100644
--- a/lib/pq/service.ts
+++ b/lib/pq/service.ts
@@ -456,10 +456,11 @@ export async function submitPQAction({
// 특정 PQ Submission ID가 있는 경우
if (pqSubmissionId) {
existingSubmission = await db
- .select({
- id: vendorPQSubmissions.id,
+ .select({
+ id: vendorPQSubmissions.id,
status: vendorPQSubmissions.status,
- type: vendorPQSubmissions.type
+ type: vendorPQSubmissions.type,
+ requesterId: vendorPQSubmissions.requesterId
})
.from(vendorPQSubmissions)
.where(
@@ -490,10 +491,11 @@ export async function submitPQAction({
}
existingSubmission = await db
- .select({
- id: vendorPQSubmissions.id,
+ .select({
+ id: vendorPQSubmissions.id,
status: vendorPQSubmissions.status,
- type: vendorPQSubmissions.type
+ type: vendorPQSubmissions.type,
+ requesterId: vendorPQSubmissions.requesterId
})
.from(vendorPQSubmissions)
.where(and(...submissionQueryConditions))
@@ -539,6 +541,7 @@ export async function submitPQAction({
submittedAt: currentDate,
createdAt: currentDate,
updatedAt: currentDate,
+ requesterId: requesterId,
});
}
@@ -556,32 +559,49 @@ export async function submitPQAction({
.where(eq(vendors.id, vendorId));
}
}
-
- // 5. 관리자에게 이메일 알림 발송
- if (process.env.ADMIN_EMAIL) {
+
+ // 5. PQ 요청자에게 이메일 알림 발송
+ const targetSubmissionId = existingSubmission?.id || '';
+ const targetRequesterId = existingSubmission?.requesterId || requesterId;
+
+ if (targetRequesterId) {
try {
- const emailSubject = projectId
- ? `[eVCP] Project PQ Submitted: ${vendor.vendorName} for ${projectName}`
- : `[eVCP] General PQ Submitted: ${vendor.vendorName}`;
-
- const adminUrl = `http://${host}/evcp/pq/${vendorId}/${existingSubmission?.id || ''}`;
-
- await sendEmail({
- to: process.env.ADMIN_EMAIL,
- subject: emailSubject,
- template: "pq-submitted-admin",
- context: {
- vendorName: vendor.vendorName,
- vendorId: vendor.id,
- projectId: projectId,
- projectName: projectName,
- isProjectPQ: !!projectId,
- submittedDate: currentDate.toLocaleString(),
- adminUrl,
- }
- });
+ // 요청자 정보 조회
+ const requester = await db
+ .select({
+ id: users.id,
+ name: users.name,
+ email: users.email,
+ })
+ .from(users)
+ .where(eq(users.id, targetRequesterId))
+ .then(rows => rows[0]);
+
+ if (requester?.email) {
+ const emailSubject = projectId
+ ? `[eVCP] Project PQ Submitted: ${vendor.vendorName} for ${projectName}`
+ : `[eVCP] General PQ Submitted: ${vendor.vendorName}`;
+
+ const adminUrl = `http://${host}/evcp/pq/${vendorId}/${targetSubmissionId}`;
+
+ await sendEmail({
+ to: requester.email,
+ subject: emailSubject,
+ template: "pq-submitted-admin",
+ context: {
+ vendorName: vendor.vendorName,
+ vendorId: vendor.id,
+ projectId: projectId,
+ projectName: projectName,
+ isProjectPQ: !!projectId,
+ submittedDate: currentDate.toLocaleString(),
+ adminUrl,
+ requesterName: requester.name,
+ }
+ });
+ }
} catch (emailError) {
- console.error("Failed to send admin notification:", emailError);
+ console.error("Failed to send requester notification:", emailError);
}
}
diff --git a/lib/site-visit/client-site-visit-wrapper.tsx b/lib/site-visit/client-site-visit-wrapper.tsx
index a23a78d7..6801445d 100644
--- a/lib/site-visit/client-site-visit-wrapper.tsx
+++ b/lib/site-visit/client-site-visit-wrapper.tsx
@@ -270,7 +270,7 @@ export function ClientSiteVisitWrapper({
<TableHead>실사기간</TableHead>
<TableHead>SHI 자료</TableHead>
<TableHead>실사요청일</TableHead>
- <TableHead>실제 실사일</TableHead>
+ {/* <TableHead>실제 실사일</TableHead> */}
<TableHead>실사결과</TableHead>
<TableHead>SHI참석자</TableHead>
@@ -357,9 +357,9 @@ export function ClientSiteVisitWrapper({
<TableCell>
{formatDateRange(request.requestedStartDate, request.requestedEndDate)}
</TableCell>
- <TableCell>
+ {/* <TableCell>
{formatDate(request.actualAt, "kr")}
- </TableCell>
+ </TableCell> */}
<TableCell>
{request.result ? (
<Badge
diff --git a/lib/site-visit/service.ts b/lib/site-visit/service.ts
index 99370cb0..d5e4a59b 100644
--- a/lib/site-visit/service.ts
+++ b/lib/site-visit/service.ts
@@ -71,13 +71,13 @@ export async function createSiteVisitRequestAction(input: {
// SHI 첨부파일 처리
if (input.attachments && input.attachments.length > 0) {
console.log(`📎 첨부파일 처리 시작: ${input.attachments.length}개 파일`);
-
- const attachmentValues = [];
-
+
+ const processedFiles: any[] = [];
+
for (const file of input.attachments) {
try {
console.log(`📁 파일 처리 중: ${file.name} (${file.size} bytes)`);
-
+
// saveDRMFile을 사용하여 파일 저장
const saveResult = await saveDRMFile(
file,
@@ -85,14 +85,14 @@ export async function createSiteVisitRequestAction(input: {
`site-visit-requests/${siteVisitRequest.id}`,
session.user.id.toString()
);
-
+
if (!saveResult.success) {
console.error(`❌ 파일 저장 실패: ${file.name}`, saveResult.error);
throw new Error(`파일 저장 실패: ${file.name} - ${saveResult.error}`);
}
-
+
console.log(`✅ 파일 저장 완료: ${file.name} -> ${saveResult.fileName}`);
-
+
// DB에 첨부파일 레코드 생성
const attachmentValue = {
siteVisitRequestId: siteVisitRequest.id,
@@ -105,18 +105,18 @@ export async function createSiteVisitRequestAction(input: {
createdAt: new Date(),
updatedAt: new Date(),
};
-
- attachmentValues.push(attachmentValue);
-
+
+ processedFiles.push(attachmentValue);
+
} catch (error) {
console.error(`❌ 첨부파일 처리 오류: ${file.name}`, error);
throw new Error(`첨부파일 처리 중 오류가 발생했습니다: ${file.name}`);
}
}
-
- if (attachmentValues.length > 0) {
- await db.insert(siteVisitRequestAttachments).values(attachmentValues);
- console.log(`✅ 첨부파일 DB 저장 완료: ${attachmentValues.length}개`);
+
+ if (processedFiles.length > 0) {
+ await db.insert(siteVisitRequestAttachments).values(processedFiles);
+ console.log(`✅ 첨부파일 DB 저장 완료: ${processedFiles.length}개`);
}
}
@@ -525,13 +525,13 @@ export async function getSiteVisitRequestAction(investigationId: number) {
.delete(siteVisitRequestAttachments)
.where(eq(siteVisitRequestAttachments.vendorSiteVisitInfoId, existingInfo[0].id));
}
-
- const attachmentValues = [];
-
+
+ let attachmentValues: any[] = [];
+
for (const file of input.attachments) {
try {
console.log(`📁 협력업체 파일 처리 중: ${file.name} (${file.size} bytes)`);
-
+
// saveFile을 사용하여 파일 저장 (협력업체 첨부파일은 일반 파일로 처리)
const saveResult = await saveFile({
file,
@@ -539,18 +539,18 @@ export async function getSiteVisitRequestAction(investigationId: number) {
originalName: file.name,
userId: session.user.id.toString()
});
-
+
if (!saveResult.success) {
console.error(`❌ 협력업체 파일 저장 실패: ${file.name}`, saveResult.error);
throw new Error(`파일 저장 실패: ${file.name} - ${saveResult.error}`);
}
-
+
console.log(`✅ 협력업체 파일 저장 완료: ${file.name} -> ${saveResult.fileName}`);
-
+
// DB에 첨부파일 레코드 생성
const attachmentValue = {
siteVisitRequestId: input.siteVisitRequestId,
- vendorSiteVisitInfoId: existingInfo.length > 0 ? existingInfo[0].id : undefined,
+ vendorSiteVisitInfoId: existingInfo.length > 0 ? existingInfo[0].id : null,
fileName: saveResult.fileName!,
originalFileName: file.name,
filePath: saveResult.publicPath!,
@@ -559,8 +559,8 @@ export async function getSiteVisitRequestAction(investigationId: number) {
createdAt: new Date(),
updatedAt: new Date(),
};
-
- attachmentValues.push(attachmentValue);
+
+ attachmentValues.push(attachmentValue as any);
} catch (error) {
console.error(`❌ 협력업체 첨부파일 처리 오류: ${file.name}`, error);
diff --git a/lib/vendor-data-plant/services copy.ts b/lib/vendor-data-plant/services copy.ts
new file mode 100644
index 00000000..7f0c47c1
--- /dev/null
+++ b/lib/vendor-data-plant/services copy.ts
@@ -0,0 +1,99 @@
+"use server";
+
+import db from "@/db/db"
+import { items } from "@/db/schema/items"
+import { projects } from "@/db/schema/projects"
+import { Tag, tags } from "@/db/schema/vendorData"
+import { eq } from "drizzle-orm"
+import { revalidateTag, unstable_noStore } from "next/cache";
+import { unstable_cache } from "@/lib/unstable-cache";
+import { contractItems, contracts } from "@/db/schema/contract";
+
+// 스키마 import
+
+export interface ProjectWithContracts {
+ projectId: number
+ projectCode: string
+ projectName: string
+ projectType: string
+
+ contracts: {
+ contractId: number
+ contractNo: string
+ contractName: string
+ // contractName 등 필요한 필드 추가
+ packages: {
+ itemId: number
+ itemName: string
+ }[]
+ }[]
+}
+
+
+export async function getVendorProjectsAndContracts(
+ vendorId: number
+): Promise<ProjectWithContracts[]> {
+ const rows = await db
+ .select({
+ projectId: projects.id,
+ projectCode: projects.code,
+ projectName: projects.name,
+ projectType: projects.type,
+
+ contractId: contracts.id,
+ contractNo: contracts.contractNo,
+ contractName: contracts.contractName,
+
+ itemId: contractItems.id,
+ itemName: items.itemName,
+ })
+ .from(contracts)
+ .innerJoin(projects, eq(contracts.projectId, projects.id))
+ .innerJoin(contractItems, eq(contractItems.contractId, contracts.id))
+ .innerJoin(items, eq(contractItems.itemId, items.id))
+ .where(eq(contracts.vendorId, vendorId))
+
+ const projectMap = new Map<number, ProjectWithContracts>()
+
+ for (const row of rows) {
+ // 1) 프로젝트 그룹 찾기
+ let projectEntry = projectMap.get(row.projectId)
+ if (!projectEntry) {
+ // 새 프로젝트 항목 생성
+ projectEntry = {
+ projectId: row.projectId,
+ projectCode: row.projectCode,
+ projectName: row.projectName,
+ projectType: row.projectType,
+ contracts: [],
+ }
+ projectMap.set(row.projectId, projectEntry)
+ }
+
+ // 2) 프로젝트 안에서 계약(contractId) 찾기
+ let contractEntry = projectEntry.contracts.find(
+ (c) => c.contractId === row.contractId
+ )
+ if (!contractEntry) {
+ // 새 계약 항목
+ contractEntry = {
+ contractId: row.contractId,
+ contractNo: row.contractNo,
+ contractName: row.contractName,
+ packages: [],
+ }
+ projectEntry.contracts.push(contractEntry)
+ }
+
+ // 3) 계약의 packages 배열에 아이템 추가
+ contractEntry.packages.push({
+ itemId: row.itemId,
+ itemName: row.itemName,
+ })
+ }
+
+ return Array.from(projectMap.values())
+}
+
+
+// 1) 태그 조회
diff --git a/lib/vendor-investigation/table/investigation-table-columns.tsx b/lib/vendor-investigation/table/investigation-table-columns.tsx
index a6c1574d..b5344a1e 100644
--- a/lib/vendor-investigation/table/investigation-table-columns.tsx
+++ b/lib/vendor-investigation/table/investigation-table-columns.tsx
@@ -84,13 +84,18 @@ export function getColumns({
id: "actions",
enableHiding: false,
cell: ({ row }) => {
+ const isCanceled = row.original.investigationStatus === "CANCELED"
+ const isCompleted = row.original.investigationStatus === "COMPLETED"
return (
<Button
variant="ghost"
className="flex size-8 p-0 data-[state=open]:bg-muted"
aria-label="실사 정보 수정"
+ disabled={isCanceled}
onClick={() => {
- setRowAction?.({ type: "update", row })
+ if (!isCanceled || !isCompleted) {
+ setRowAction?.({ type: "update", row })
+ }
}}
>
<Edit className="size-4" aria-hidden="true" />
diff --git a/lib/vendor-regular-registrations/service.ts b/lib/vendor-regular-registrations/service.ts
index 08e5c9ef..e163b147 100644
--- a/lib/vendor-regular-registrations/service.ts
+++ b/lib/vendor-regular-registrations/service.ts
@@ -311,7 +311,8 @@ export async function sendMissingContractRequestEmails(vendorIds: number[]) {
const headersList = await headers();
const host = headersList.get('host') || 'localhost:3000';
- const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
+ // const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
+ const protocol ='http';// 운영 시점에서는 https로 변경
const baseUrl = `${protocol}://${host}`;
const contractManagementUrl = `${baseUrl}/ko/partners/basic-contract`; // 실제 기본계약 관리 페이지 URL로 수정 필요
@@ -392,7 +393,8 @@ export async function sendAdditionalInfoRequestEmails(vendorIds: number[]) {
const headersList = await headers();
const host = headersList.get('host') || 'localhost:3000';
- const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
+ // const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
+ const protocol ='http';// 운영 시점에서는 https로 변경
const baseUrl = `${protocol}://${host}`;
const vendorInfoUrl = `${baseUrl}/ko/partners/info`; // 실제 업체정보 관리 페이지 URL로 수정 필요
diff --git a/lib/vendors/table/request-pq-dialog.tsx b/lib/vendors/table/request-pq-dialog.tsx
index b5e3b8a8..2f39cae1 100644
--- a/lib/vendors/table/request-pq-dialog.tsx
+++ b/lib/vendors/table/request-pq-dialog.tsx
@@ -145,22 +145,35 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
setIsLoadingTemplates(true)
getALLBasicContractTemplates()
.then((templates) => {
- setBasicContractTemplates(templates)
// 벤더 국가별 자동 선택 로직
if (vendors.length > 0) {
const isAllForeign = vendors.every(vendor => vendor.country !== 'KR')
const isAllDomestic = vendors.every(vendor => vendor.country === 'KR')
-
+ //외자면 리스트에 비밀유지계약, 기술자료, 내국신용장, 한글 제외
+ if(isAllForeign) {
+ const foreignTemplates = templates.filter(template => {
+ const name = template.templateName?.toLowerCase() || ''
+ return !name.includes('비밀유지') && !name.includes('기술자료') && !name.includes('내국신용장') && !name.includes('한글')
+ })
+ setBasicContractTemplates(foreignTemplates)
+ }
+ //내자면 리스트에 GTC 제외, 비밀유지, 기술자료, 영문서약 제외
+ if(isAllDomestic) {
+ const domesticTemplates = templates.filter(template => {
+ const name = template.templateName?.toLowerCase() || ''
+ return !name.includes('gtc') && !name.includes('비밀유지') && !name.includes('기술자료') && !name.includes('영문')
+ })
+ setBasicContractTemplates(domesticTemplates)
+ }
if (isAllForeign) {
// 외자: 준법서약 (영문), GTC 선택 (GTC는 1개만 선택하도록)
- // 비밀유지 계약서, 기술자료 요구서 제외
const foreignTemplates = templates.filter(template => {
const name = template.templateName?.toLowerCase() || ''
return (
(template.templateName?.includes('준법서약') && template.templateName?.includes('영문')) ||
template.templateName?.includes('gtc')
- ) && !name.includes('비밀유지') && !name.includes('기술자료')
+ )
})
// GTC 템플릿 중 최신 리비전의 것만 선택
const gtcTemplates = foreignTemplates.filter(t => t.templateName?.includes('gtc'))
@@ -181,9 +194,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
const domesticTemplates = templates.filter(template => {
const name = template.templateName?.toLowerCase() || ''
return !(name.includes('준법서약') && name.includes('영문')) &&
- !name.includes('gtc') &&
- !name.includes('비밀유지') &&
- !name.includes('기술자료')
+ !name.includes('gtc')
})
setSelectedTemplateIds(domesticTemplates.map(t => t.id))
}
diff --git a/lib/vendors/table/vendors-table.tsx b/lib/vendors/table/vendors-table.tsx
index 34b9b3e7..894ee3d4 100644
--- a/lib/vendors/table/vendors-table.tsx
+++ b/lib/vendors/table/vendors-table.tsx
@@ -115,6 +115,7 @@ export function VendorsTable({ promises }: VendorsTableProps) {
filterFields,
enablePinning: true,
enableAdvancedFilter: true,
+ enableMultiRowSelection: false, // 단일 선택 모드로 제한
initialState: {
sorting: [{ id: "createdAt", desc: true }],
columnPinning: { right: ["actions"] },