diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-29 06:20:56 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-29 06:20:56 +0000 |
| commit | 2fc9e5492e220041ba322d9a1479feb7803228cf (patch) | |
| tree | da8ace07ed23ba92f2408c9c6e9ae2e31be20160 /lib/vendors/table | |
| parent | 5202c4b56d9565c7ac0c2a62255763462cef0d3d (diff) | |
(최겸) 구매 PQ수정, 정규업체 결재 개발(진행중)
Diffstat (limited to 'lib/vendors/table')
| -rw-r--r-- | lib/vendors/table/request-pq-dialog.tsx | 277 |
1 files changed, 157 insertions, 120 deletions
diff --git a/lib/vendors/table/request-pq-dialog.tsx b/lib/vendors/table/request-pq-dialog.tsx index 2f39cae1..07057dbe 100644 --- a/lib/vendors/table/request-pq-dialog.tsx +++ b/lib/vendors/table/request-pq-dialog.tsx @@ -46,10 +46,11 @@ import { useSession } from "next-auth/react" import { DatePicker } from "@/components/ui/date-picker"
import { getALLBasicContractTemplates } from "@/lib/basic-contract/service"
import type { BasicContractTemplate } from "@/db/schema"
-import { searchItemsForPQ } from "@/lib/items/service"
-import { saveNdaAttachments } from "../service"
+import { saveNdaAttachments, getVendorPQHistory } from "../service"
import { useRouter } from "next/navigation"
import { createGtcVendorDocuments, createProjectGtcVendorDocuments, getStandardGtcDocumentId, getProjectGtcDocumentId } from "@/lib/gtc-contract/service"
+import { MaterialGroupSelectorDialogMulti } from "@/components/common/material/material-group-selector-dialog-multi"
+import type { MaterialSearchItem } from "@/lib/material/material-group-service"
// import { PQContractViewer } from "../pq-contract-viewer" // 더 이상 사용하지 않음
interface RequestPQDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> {
@@ -69,11 +70,7 @@ interface RequestPQDialogProps extends React.ComponentPropsWithoutRef<typeof Dia // "GTC 합의",
// ]
-// PQ 대상 품목 타입 정의
-interface PQItem {
- itemCode: string
- itemName: string
-}
+// PQ 대상 품목 타입 정의 (Material Group 기반) - MaterialSearchItem 사용
export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...props }: RequestPQDialogProps) {
const [isApprovePending, startApproveTransition] = React.useTransition()
@@ -86,12 +83,9 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro const [selectedProjectId, setSelectedProjectId] = React.useState<number | null>(null)
const [agreements, setAgreements] = React.useState<Record<string, boolean>>({})
const [extraNote, setExtraNote] = React.useState<string>("")
- const [pqItems, setPqItems] = React.useState<PQItem[]>([])
+ const [pqItems, setPqItems] = React.useState<MaterialSearchItem[]>([])
- // 아이템 검색 관련 상태
- const [itemSearchQuery, setItemSearchQuery] = React.useState<string>("")
- const [filteredItems, setFilteredItems] = React.useState<PQItem[]>([])
- const [showItemDropdown, setShowItemDropdown] = React.useState<boolean>(false)
+ // PQ 품목 선택 관련 상태는 MaterialGroupSelectorDialogMulti에서 관리됨
const [isLoadingProjects, setIsLoadingProjects] = React.useState(false)
const [basicContractTemplates, setBasicContractTemplates] = React.useState<BasicContractTemplate[]>([])
const [selectedTemplateIds, setSelectedTemplateIds] = React.useState<number[]>([])
@@ -106,31 +100,10 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro const [currentStep, setCurrentStep] = React.useState("")
const [showProgress, setShowProgress] = React.useState(false)
- // 아이템 검색 필터링
- React.useEffect(() => {
- if (itemSearchQuery.trim() === "") {
- setFilteredItems([])
- setShowItemDropdown(false)
- return
- }
+ // PQ 히스토리 관련 상태
+ const [pqHistory, setPqHistory] = React.useState<Record<number, any[]>>({})
+ const [isLoadingHistory, setIsLoadingHistory] = React.useState(false)
- const searchItems = async () => {
- try {
- const results = await searchItemsForPQ(itemSearchQuery)
- setFilteredItems(results)
- setShowItemDropdown(true)
- } catch (error) {
- console.error("아이템 검색 오류:", error)
- toast.error("아이템 검색 중 오류가 발생했습니다.")
- setFilteredItems([])
- setShowItemDropdown(false)
- }
- }
-
- // 디바운싱: 300ms 후에 검색 실행
- const timeoutId = setTimeout(searchItems, 300)
- return () => clearTimeout(timeoutId)
- }, [itemSearchQuery])
React.useEffect(() => {
if (type === "PROJECT") {
@@ -140,9 +113,37 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro }
}, [type])
- // 기본계약서 템플릿 로딩 및 자동 선택
+ // 기본계약서 템플릿 로딩 및 자동 선택, PQ 히스토리 로딩
React.useEffect(() => {
setIsLoadingTemplates(true)
+ const loadPQHistory = async () => {
+ if (vendors.length === 0) return
+
+ setIsLoadingHistory(true)
+ try {
+ const historyPromises = vendors.map(async (vendor) => {
+ console.log("vendor.id", vendor.id)
+ const result = await getVendorPQHistory(vendor.id)
+ console.log("result", result)
+ return { vendorId: vendor.id, history: result.success ? result.data : [] }
+ })
+
+ const results = await Promise.all(historyPromises)
+ const historyMap: Record<number, any[]> = {}
+
+ results.forEach(({ vendorId, history }) => {
+ historyMap[vendorId] = history
+ })
+
+ setPqHistory(historyMap)
+ } catch (error) {
+ console.error('PQ 히스토리 로딩 실패:', error)
+ toast.error('PQ 히스토리 로딩 중 오류가 발생했습니다')
+ } finally {
+ setIsLoadingHistory(false)
+ }
+ }
+ loadPQHistory()
getALLBasicContractTemplates()
.then((templates) => {
@@ -213,37 +214,24 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro setPqItems([])
setExtraNote("")
setSelectedTemplateIds([])
- setItemSearchQuery("")
- setFilteredItems([])
- setShowItemDropdown(false)
setNdaAttachments([])
setIsUploadingNdaFiles(false)
setProgressValue(0)
setCurrentStep("")
setShowProgress(false)
+ setPqHistory({})
+ setIsLoadingHistory(false)
}
}, [props.open])
- // 아이템 선택 함수
- const handleSelectItem = (item: PQItem) => {
- // 이미 선택된 아이템인지 확인
- const isAlreadySelected = pqItems.some(selectedItem =>
- selectedItem.itemCode === item.itemCode
- )
-
- if (!isAlreadySelected) {
- setPqItems(prev => [...prev, item])
- }
-
- // 검색 초기화
- setItemSearchQuery("")
- setFilteredItems([])
- setShowItemDropdown(false)
+ // PQ 품목 선택 함수 (MaterialGroupSelectorDialogMulti에서 호출됨)
+ const handlePQItemsChange = (items: MaterialSearchItem[]) => {
+ setPqItems(items)
}
- // 아이템 제거 함수
- const handleRemoveItem = (itemCode: string) => {
- setPqItems(prev => prev.filter(item => item.itemCode !== itemCode))
+ // PQ 품목 제거 함수
+ const handleRemovePQItem = (materialGroupCode: string) => {
+ setPqItems(prev => prev.filter(item => item.materialGroupCode !== materialGroupCode))
}
// 비밀유지 계약서 첨부파일 추가 함수
@@ -274,6 +262,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro if (!type) return toast.error("PQ 유형을 선택하세요.")
if (type === "PROJECT" && !selectedProjectId) return toast.error("프로젝트를 선택하세요.")
if (!dueDate) return toast.error("마감일을 선택하세요.")
+ if (pqItems.length === 0) return toast.error("PQ 대상 품목을 선택하세요.")
if (!session?.user?.id) return toast.error("인증 실패")
// GTC 템플릿 선택 검증
@@ -317,7 +306,10 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro projectId: type === "PROJECT" ? selectedProjectId : null,
type: type || "GENERAL",
extraNote,
- pqItems: JSON.stringify(pqItems),
+ pqItems: JSON.stringify(pqItems.map(item => ({
+ materialGroupCode: item.materialGroupCode,
+ materialGroupDescription: item.materialGroupDescription
+ }))),
templateId: selectedTemplateIds.length > 0 ? selectedTemplateIds[0] : null,
})
@@ -660,9 +652,97 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro toast.error(`기본계약서 이메일 발송 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`)
}
}
-
-
+ // PQ 히스토리 컴포넌트
+ const PQHistorySection = () => {
+ if (isLoadingHistory) {
+ return (
+ <div className="px-4 py-3 border-b bg-muted/30">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <Loader className="h-4 w-4 animate-spin" />
+ PQ 히스토리 로딩 중...
+ </div>
+ </div>
+ )
+ }
+
+ const hasAnyHistory = Object.values(pqHistory).some(history => history.length > 0)
+
+ if (!hasAnyHistory) {
+ return (
+ <div className="px-4 py-3 border-b bg-muted/30">
+ <div className="text-sm text-muted-foreground">
+ 최근 PQ 요청 내역이 없습니다.
+ </div>
+ </div>
+ )
+ }
+
+ return (
+ <div className="px-4 py-3 border-b bg-muted/30 max-h-48 overflow-y-auto">
+ <div className="space-y-3">
+ <div className="text-sm font-medium text-muted-foreground">
+ 최근 PQ 요청 내역
+ </div>
+ {vendors.map((vendor) => {
+ const vendorHistory = pqHistory[vendor.id] || []
+ if (vendorHistory.length === 0) return null
+
+ return (
+ <div key={vendor.id} className="space-y-2">
+ <div className="text-xs font-medium text-muted-foreground border-b pb-1">
+ {vendor.vendorName}
+ </div>
+ <div className="space-y-1">
+ {vendorHistory.slice(0, 3).map((pq) => {
+ const createdDate = new Date(pq.createdAt).toLocaleDateString('ko-KR')
+ const statusText =
+ pq.status === 'REQUESTED' ? '요청됨' :
+ pq.status === 'APPROVED' ? '승인됨' :
+ pq.status === 'SUBMITTED' ? '제출됨' :
+ pq.status === 'REJECTED' ? '거절됨' :
+ pq.status
+
+ return (
+ <div key={pq.id} className="flex items-center justify-between text-xs bg-background rounded px-2 py-1">
+ <div className="flex items-center gap-2 flex-1">
+ <button
+ type="button"
+ onClick={() => router.push(`/evcp/pq_new?search=${encodeURIComponent(pq.pqNumber)}`)}
+ className="font-mono text-blue-600 hover:text-blue-800 hover:underline cursor-pointer"
+ >
+ {pq.pqNumber}
+ </button>
+ <Badge variant={pq.status === 'SUBMITTED' ? 'default' : pq.status === 'COMPLETED' ? 'default' : 'outline'} className="text-xs">
+ {statusText}
+ </Badge>
+ </div>
+ <div className="text-right">
+ <div className="text-muted-foreground">
+ {pq.type === 'GENERAL' ? '일반' : pq.type === 'PROJECT' ? '프로젝트' : '미실사'}
+ </div>
+ <div className="text-muted-foreground text-xs">
+ {createdDate}
+ </div>
+ </div>
+ </div>
+ )
+ })}
+ {vendorHistory.length > 3 && (
+ <div className="text-xs text-muted-foreground text-center">
+ 외 {vendorHistory.length - 3}건 더 있음
+ </div>
+ )}
+ </div>
+ </div>
+ )
+ })}
+ </div>
+ </div>
+ )
+ }
+
+
const dialogContent = (
<div className="space-y-4 py-2">
@@ -734,68 +814,23 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro {/* PQ 대상품목 */}
<div className="space-y-2">
- <Label>PQ 대상품목</Label>
-
- {/* 선택된 아이템들 표시 */}
+ <Label>PQ 대상품목 *</Label>
+ <br />
+ <MaterialGroupSelectorDialogMulti
+ triggerLabel="자재 그룹 선택"
+ selectedMaterials={pqItems}
+ onMaterialsSelect={handlePQItemsChange}
+ maxSelections={10}
+ placeholder="PQ 대상 자재 그룹을 검색하세요"
+ title="PQ 대상 자재 그룹 선택"
+ description="PQ를 요청할 자재 그룹을 선택해주세요."
+ />
+
{pqItems.length > 0 && (
- <div className="flex flex-wrap gap-2 mb-2">
- {pqItems.map((item) => (
- <Badge key={item.itemCode} variant="secondary" className="flex items-center gap-1">
- <span className="text-xs">
- {item.itemCode} - {item.itemName}
- </span>
- <Button
- type="button"
- variant="ghost"
- size="sm"
- className="h-4 w-4 p-0 hover:bg-destructive hover:text-destructive-foreground"
- onClick={() => handleRemoveItem(item.itemCode)}
- >
- <X className="h-3 w-3" />
- </Button>
- </Badge>
- ))}
+ <div className="text-xs text-muted-foreground">
+ {pqItems.length}개 자재 그룹이 선택되었습니다.
</div>
)}
-
- {/* 검색 입력 */}
- <div className="relative">
- <div className="relative">
- <Input
- placeholder="아이템 코드 또는 이름으로 검색하세요"
- value={itemSearchQuery}
- onChange={(e) => setItemSearchQuery(e.target.value)}
- className="pl-9"
- />
- </div>
-
- {/* 검색 결과 드롭다운 */}
- {showItemDropdown && (
- <div className="absolute top-full left-0 right-0 z-50 mt-1 max-h-48 overflow-y-auto bg-background border rounded-md shadow-lg">
- {filteredItems.length > 0 ? (
- filteredItems.map((item) => (
- <button
- key={item.itemCode}
- type="button"
- className="w-full px-3 py-2 text-left text-sm hover:bg-muted focus:bg-muted focus:outline-none"
- onClick={() => handleSelectItem(item)}
- >
- <div className="font-medium">{item.itemCode}</div>
- <div className="text-muted-foreground text-xs">{item.itemName}</div>
- </button>
- ))
- ) : (
- <div className="px-3 py-2 text-sm text-muted-foreground">
- 검색 결과가 없습니다.
- </div>
- )}
- </div>
- )}
- </div>
-
- <div className="text-xs text-muted-foreground">
- 아이템 코드나 이름을 입력하여 검색하고 선택하세요. (선택사항)
- </div>
</div>
{/* 추가 안내사항 */}
@@ -957,6 +992,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro {vendors.length === 1 ? "개 협력업체" : "개 협력업체들"}에게 PQ를 요청합니다.
</DialogDescription>
</DialogHeader>
+ <PQHistorySection />
<div className="flex-1 overflow-y-auto">
{dialogContent}
</div>
@@ -1010,6 +1046,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro {vendors.length === 1 ? "개 협력업체" : "개 협력업체들"}에게 PQ를 요청합니다.
</DrawerDescription>
</DrawerHeader>
+ <PQHistorySection />
<div className="flex-1 overflow-y-auto px-4">
{dialogContent}
</div>
|
