diff options
25 files changed, 279 insertions, 493 deletions
diff --git a/app/[lng]/evcp/(evcp)/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx index c63cf4df..8e3006cc 100644 --- a/app/[lng]/evcp/(evcp)/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx +++ b/app/[lng]/evcp/(evcp)/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx @@ -87,12 +87,12 @@ export default async function PQReviewPage(props: PQReviewPageProps) { <Shell className="gap-6 max-w-5xl">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
- <Button variant="outline" size="sm" asChild>
+ {/* <Button variant="outline" size="sm" asChild>
<Link href="/evcp/pq_new">
<ArrowLeft className="w-4 h-4 mr-2" />
목록으로
</Link>
- </Button>
+ </Button> */}
<div>
<h2 className="text-2xl font-bold tracking-tight">
{pqSubmission.vendorName} - {typeLabel}
diff --git a/components/bidding/create/bidding-create-dialog.tsx b/components/bidding/create/bidding-create-dialog.tsx index 4ef403c9..30550ca4 100644 --- a/components/bidding/create/bidding-create-dialog.tsx +++ b/components/bidding/create/bidding-create-dialog.tsx @@ -43,6 +43,7 @@ import { ProcurementManagerSelector } from '@/components/common/selectors/procur import type { PurchaseGroupCodeWithUser } from '@/components/common/selectors/purchase-group-code/purchase-group-code-service'
import type { ProcurementManagerWithUser } from '@/components/common/selectors/procurement-manager/procurement-manager-service'
import { createBidding } from '@/lib/bidding/service'
+import { useSession } from 'next-auth/react'
interface BiddingCreateDialogProps {
form: UseFormReturn<CreateBiddingSchema>
@@ -50,6 +51,9 @@ interface BiddingCreateDialogProps { }
export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProps) {
+ const { data: session } = useSession()
+ const userId = session?.user?.id ? Number(session.user.id) : null;
+
const [isOpen, setIsOpen] = React.useState(false)
const [isSubmitting, setIsSubmitting] = React.useState(false)
@@ -290,7 +294,7 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp },
}
- const result = await createBidding(biddingData, '1') // 실제로는 현재 사용자 ID
+ const result = await createBidding(biddingData, userId?.toString() || '') // 실제로는 현재 사용자 ID
if (result.success) {
toast.success("입찰이 성공적으로 생성되었습니다.")
@@ -1147,9 +1151,6 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp 찾아보세요
</label>
</p>
- <p className="text-xs text-gray-500">
- PDF, DOC, DOCX, XLS, XLSX, PNG, JPG, JPEG 파일을 업로드할 수 있습니다
- </p>
</div>
</div>
</div>
@@ -1217,9 +1218,6 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp 찾아보세요
</label>
</p>
- <p className="text-xs text-gray-500">
- PDF, DOC, DOCX, XLS, XLSX, PNG, JPG, JPEG 파일을 업로드할 수 있습니다
- </p>
</div>
</div>
</div>
diff --git a/components/bidding/manage/bidding-basic-info-editor.tsx b/components/bidding/manage/bidding-basic-info-editor.tsx index a956d73c..2f55d563 100644 --- a/components/bidding/manage/bidding-basic-info-editor.tsx +++ b/components/bidding/manage/bidding-basic-info-editor.tsx @@ -1199,9 +1199,6 @@ export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProp 찾아보세요 </label> </p> - <p className="text-xs text-gray-500"> - PDF, DOC, DOCX, XLS, XLSX, PNG, JPG, JPEG 파일을 업로드할 수 있습니다 - </p> </div> </div> </div> @@ -1334,9 +1331,6 @@ export function BiddingBasicInfoEditor({ biddingId }: BiddingBasicInfoEditorProp 찾아보세요 </label> </p> - <p className="text-xs text-gray-500"> - PDF, DOC, DOCX, XLS, XLSX, PNG, JPG, JPEG 파일을 업로드할 수 있습니다 - </p> </div> </div> </div> diff --git a/components/bidding/manage/bidding-schedule-editor.tsx b/components/bidding/manage/bidding-schedule-editor.tsx index d64c16c0..ce03c742 100644 --- a/components/bidding/manage/bidding-schedule-editor.tsx +++ b/components/bidding/manage/bidding-schedule-editor.tsx @@ -108,7 +108,7 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps) }) const [isLoading, setIsLoading] = React.useState(false) const [isSubmitting, setIsSubmitting] = React.useState(false) - const [biddingInfo, setBiddingInfo] = React.useState<{ title: string; projectName?: string } | null>(null) + const [biddingInfo, setBiddingInfo] = React.useState<{ title: string; projectName?: string; status: string } | null>(null) const [isBiddingInvitationDialogOpen, setIsBiddingInvitationDialogOpen] = React.useState(false) const [selectedVendors, setSelectedVendors] = React.useState<VendorContractRequirement[]>([]) @@ -123,6 +123,7 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps) setBiddingInfo({ title: bidding.title || '', projectName: bidding.projectName || undefined, + status: bidding.status || '', }) // 날짜를 문자열로 변환하는 헬퍼 @@ -617,7 +618,7 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps) <Button variant="default" onClick={() => setIsBiddingInvitationDialogOpen(true)} - disabled={!biddingInfo} + disabled={!biddingInfo || biddingInfo.status !== 'bidding_generated'} className="min-w-[120px]" > <Send className="w-4 h-4 mr-2" /> @@ -626,7 +627,7 @@ export function BiddingScheduleEditor({ biddingId }: BiddingScheduleEditorProps) <div className="flex gap-4"> <Button onClick={handleSave} - disabled={isSubmitting} + disabled={isSubmitting || !biddingInfo || biddingInfo.status !== 'bidding_generated'} className="min-w-[120px]" > {isSubmitting ? ( diff --git a/components/vendor-regular-registrations/document-status-dialog.tsx b/components/vendor-regular-registrations/document-status-dialog.tsx index 3073a651..5efee64e 100644 --- a/components/vendor-regular-registrations/document-status-dialog.tsx +++ b/components/vendor-regular-registrations/document-status-dialog.tsx @@ -229,8 +229,8 @@ export function DocumentStatusDialog({ return (
<Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
- <DialogHeader>
+ <DialogContent className="max-w-4xl max-h-[80vh]">
+ <DialogHeader className="sticky top-0 z-10 border-b pr-4 pb-4 mb-4">
<DialogTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FileText className="w-5 h-5" />
@@ -250,7 +250,7 @@ export function DocumentStatusDialog({ </DialogTitle>
</DialogHeader>
- <div className="space-y-6">
+ <div className="overflow-y-auto max-h-[calc(80vh-120px)] space-y-6">
{/* 기본 정보 */}
<div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg">
<div>
diff --git a/i18n/locales/en/menu.json b/i18n/locales/en/menu.json index 9b559201..368dbd92 100644 --- a/i18n/locales/en/menu.json +++ b/i18n/locales/en/menu.json @@ -130,6 +130,7 @@ "legalReview":"Legal Review Status", "legalResponse":"Legal Response", "vendor_regular_registrations": "Regular Vendor Registration Management", + "vendor_regular_registrations_desc": "Check and manage regular vendor registration status", "vendor_consent": "Consent", "vendor_consent_desc": "Terms and Conditions" }, diff --git a/lib/approval/templates/실사의뢰 및 실사재의뢰 요청.html b/lib/approval/templates/실사의뢰 및 실사재의뢰 요청.html index e2a7cf6d..dc5e5fd5 100644 --- a/lib/approval/templates/실사의뢰 및 실사재의뢰 요청.html +++ b/lib/approval/templates/실사의뢰 및 실사재의뢰 요청.html @@ -29,7 +29,7 @@ font-weight: 700; " > - VENDOR 실사의뢰 (재의뢰) + VENDOR 실사의뢰 </th> </tr> </thead> diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts index d0c7a0cd..5909cd62 100644 --- a/lib/bidding/actions.ts +++ b/lib/bidding/actions.ts @@ -613,11 +613,11 @@ export async function cancelDisposalAction( } } - // 3. 입찰 상태를 입찰 진행중으로 변경 + // 3. 입찰 상태를 입찰생성으로 변경 await tx .update(biddings) .set({ - status: 'evaluation_of_bidding', + status: 'bidding_generated', updatedAt: new Date(), updatedBy: userName, }) @@ -734,7 +734,7 @@ export async function earlyOpenBiddingAction(biddingId: number) { // 5. 참여협력사 중 최종응찰 버튼을 클릭한 업체들만 있는지 검증 // bidding_submitted 상태인 업체들이 있는지 확인 (이미 위에서 검증됨) - // 6. 조기개찰 상태로 변경 + // 6. 입찰평가중 상태로 변경 await tx .update(biddings) .set({ diff --git a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx index e3db8861..491f29f7 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx @@ -180,8 +180,8 @@ export function BiddingDetailVendorToolbarActions({ <> <div className="flex items-center gap-2"> {/* 상태별 액션 버튼 */} - {/* 차수증가: 입찰공고 또는 입찰 진행중 상태 */} - {(bidding.status === 'bidding_generated' || bidding.status === 'bidding_opened') && ( + {/* 차수증가: 입찰평가중 또는 입찰 진행중 상태 */} + {(bidding.status === 'evaluation_of_bidding ' || bidding.status === 'bidding_opened') && ( <Button variant="outline" size="sm" diff --git a/lib/bidding/pre-quote/service.ts b/lib/bidding/pre-quote/service.ts index ea92f294..19b418ae 100644 --- a/lib/bidding/pre-quote/service.ts +++ b/lib/bidding/pre-quote/service.ts @@ -1492,18 +1492,15 @@ export async function sendBiddingBasicContracts( try {
await sendEmail({
to: vendor.selectedMainEmail,
- template: 'basic-contract-notification',
+ subject: `[eVCP] 기본계약서 서명 요청`,
+ template: "contract-sign-request",
context: {
vendorName: vendor.vendorName,
- biddingId: biddingId,
- contractCount: contractTypes.length,
- deadline: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000).toLocaleDateString('ko-KR'),
- loginUrl: `${process.env.NEXT_PUBLIC_APP_URL}/partners/bid/${biddingId}`,
- message: message || '',
- currentYear: new Date().getFullYear(),
- language: 'ko'
- }
- })
+ templateName: contractTypes.map(ct => ct.templateName).join(', '),
+ loginUrl: `${process.env.NEXT_PUBLIC_APP_URL}/partners/basic-contract`,
+ language:'ko'
+ },
+ });
} catch (emailError) {
console.error(`이메일 발송 실패 (${vendor.selectedMainEmail}):`, emailError)
// 이메일 발송 실패해도 계약 생성은 유지
diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index a7cd8286..b60fc73d 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -33,6 +33,11 @@ import { like, notInArray } from 'drizzle-orm' +import { revalidatePath } from 'next/cache' +import { filterColumns } from '@/lib/filter-columns' +import { GetBiddingsSchema } from './validation' + + // 사용자 이메일로 사용자 코드 조회 export async function getUserCodeByEmail(email: string): Promise<string | null> { @@ -49,9 +54,6 @@ export async function getUserCodeByEmail(email: string): Promise<string | null> return null } } -import { revalidatePath } from 'next/cache' -import { filterColumns } from '@/lib/filter-columns' -import { CreateBiddingSchema, GetBiddingsSchema, UpdateBiddingSchema } from './validation' import { saveFile } from '../file-stroage' // userId를 user.name으로 변환하는 유틸리티 함수 @@ -973,6 +975,74 @@ function generateNextSequence(currentMax: string | null): string { return result.padStart(4, '0'); } +// 입찰 참여 현황 카운트 계산 함수 +export async function getParticipantCountsForBidding(biddingId: number) { + try { + // 전체 참여자 수 (예상 참여자) + const expectedResult = await db + .select({ count: count() }) + .from(biddingCompanies) + .where(eq(biddingCompanies.biddingId, biddingId)) + + const expected = expectedResult[0]?.count || 0 + + // 참여 완료 수 + const participatedResult = await db + .select({ count: count() }) + .from(biddingCompanies) + .where(and( + eq(biddingCompanies.biddingId, biddingId), + eq(biddingCompanies.invitationStatus, 'bidding_submitted') + )) + + const participated = participatedResult[0]?.count || 0 + + // 거부/취소 수 + const declinedResult = await db + .select({ count: count() }) + .from(biddingCompanies) + .where(and( + eq(biddingCompanies.biddingId, biddingId), + or( + eq(biddingCompanies.invitationStatus, 'bidding_declined'), + eq(biddingCompanies.invitationStatus, 'bidding_cancelled') + ) + )) + + const declined = declinedResult[0]?.count || 0 + + // 대기중 수 + const pendingResult = await db + .select({ count: count() }) + .from(biddingCompanies) + .where(and( + eq(biddingCompanies.biddingId, biddingId), + or( + eq(biddingCompanies.invitationStatus, 'pending'), + eq(biddingCompanies.invitationStatus, 'bidding_sent'), + eq(biddingCompanies.invitationStatus, 'bidding_accepted') + ) + )) + + const pending = pendingResult[0]?.count || 0 + + return { + participantExpected: expected, + participantParticipated: participated, + participantDeclined: declined, + participantPending: pending + } + } catch (error) { + console.error('Error in getParticipantCountsForBidding:', error) + return { + participantExpected: 0, + participantParticipated: 0, + participantDeclined: 0, + participantPending: 0 + } + } +} + // 자동 입찰번호 생성 export async function generateBiddingNumber( contractType: string, @@ -982,9 +1052,9 @@ export async function generateBiddingNumber( ): Promise<string> { // 계약 타입별 접두사 설정 const typePrefix = { - 'general': 'E', - 'unit_price': 'F', - 'sale': 'G' + 'general': 'E', // 일반계약 + 'unit_price': 'F', // 단가계약 + 'sale': 'G', // 매각계약 }; const prefix = typePrefix[contractType as keyof typeof typePrefix] || 'E'; @@ -3163,8 +3233,8 @@ export async function increaseRoundOrRebid(biddingId: number, userId: string | u prNumber: existingBidding.prNumber, hasPrDocument: existingBidding.hasPrDocument, - // 상태는 내정가 산정으로 초기화 - status: 'set_target_price', + // 상태는 입찰생성으로 초기화 + status: 'bidding_generated', isPublic: existingBidding.isPublic, isUrgent: existingBidding.isUrgent, @@ -3530,8 +3600,8 @@ export async function getBiddingsForReceive(input: GetBiddingsSchema) { orderByColumns.push(desc(biddings.createdAt)) } - // bid-receive 페이지용 데이터 조회 (필요한 컬럼만 선택) - const data = await db + // bid-receive 페이지용 데이터 조회 (참여 현황 제외하고 기본 정보만) + const biddingData = await db .select({ // 기본 입찰 정보 id: biddings.id, @@ -3549,42 +3619,6 @@ export async function getBiddingsForReceive(input: GetBiddingsSchema) { createdAt: biddings.createdAt, updatedAt: biddings.updatedAt, - // 참여 현황 집계 - participantExpected: sql<number>` - COALESCE(( - SELECT count(*) - FROM bidding_companies - WHERE bidding_id = ${biddings.id} - ), 0) - `.as('participant_expected'), - - participantParticipated: sql<number>` - COALESCE(( - SELECT count(*) - FROM bidding_companies - WHERE bidding_id = ${biddings.id} - AND invitation_status = 'bidding_submitted' - ), 0) - `.as('participant_participated'), - - participantDeclined: sql<number>` - COALESCE(( - SELECT count(*) - FROM bidding_companies - WHERE bidding_id = ${biddings.id} - AND invitation_status IN ('bidding_declined', 'bidding_cancelled') - ), 0) - `.as('participant_declined'), - - participantPending: sql<number>` - COALESCE(( - SELECT count(*) - FROM bidding_companies - WHERE bidding_id = ${biddings.id} - AND invitation_status IN ('pending', 'bidding_sent', 'bidding_accepted') - ), 0) - `.as('participant_pending'), - // 개찰 정보 openedAt: biddings.openedAt, openedBy: biddings.openedBy, @@ -3595,6 +3629,17 @@ export async function getBiddingsForReceive(input: GetBiddingsSchema) { .limit(input.perPage) .offset(offset) + // 각 입찰에 대한 참여 현황 카운트 계산 + const data = await Promise.all( + biddingData.map(async (bidding) => { + const participantCounts = await getParticipantCountsForBidding(bidding.id) + return { + ...bidding, + ...participantCounts + } + }) + ) + const pageCount = Math.ceil(total / input.perPage) return { data, pageCount, total } diff --git a/lib/itb/service.ts b/lib/itb/service.ts index 181285cc..882210b3 100644 --- a/lib/itb/service.ts +++ b/lib/itb/service.ts @@ -719,7 +719,7 @@ export async function approvePurchaseRequestsAndCreateRfqs( rfqsLastId: rfq.id, rfqItem: `${index + 1}`.padStart(3, '0'), prItem: `${index + 1}`.padStart(3, '0'), - prNo: rfqCode, + prNo: '-', materialCategory: request.majorItemMaterialCategory, materialCode: item.itemCode, materialDescription: item.itemName, diff --git a/lib/pq/pq-review-table-new/request-investigation-dialog.tsx b/lib/pq/pq-review-table-new/request-investigation-dialog.tsx index aaf10a71..f5a7ff91 100644 --- a/lib/pq/pq-review-table-new/request-investigation-dialog.tsx +++ b/lib/pq/pq-review-table-new/request-investigation-dialog.tsx @@ -59,7 +59,7 @@ const requestInvestigationFormSchema = z.object({ }),
investigationAddress: z.string().min(1, "실사 장소를 입력해주세요."),
investigationMethod: z.string().optional(),
- investigationNotes: z.string().optional(),
+ investigationNotes: z.string().min(1, "실사 목적을 입력해주세요."),
})
type RequestInvestigationFormValues = z.infer<typeof requestInvestigationFormSchema>
@@ -264,10 +264,10 @@ export function RequestInvestigationDialog({ name="investigationNotes"
render={({ field }) => (
<FormItem>
- <FormLabel>특이사항 (선택사항)</FormLabel>
+ <FormLabel>실사목적</FormLabel>
<FormControl>
<Textarea
- placeholder="실사 관련 특이사항을 입력하세요"
+ placeholder="실사 목적을 입력하세요"
className="resize-none min-h-[60px]"
{...field}
disabled={isPending}
diff --git a/lib/pq/service.ts b/lib/pq/service.ts index fd71888e..57ce5f88 100644 --- a/lib/pq/service.ts +++ b/lib/pq/service.ts @@ -2821,7 +2821,7 @@ export async function requestPqSupplementAction({ requesterEmail = requester?.email || null;
}
- const reviewUrl = `http://${host}/evcp/pq/${vendorId}/${pqSubmissionId}`;
+ const reviewUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/partners/pq_new`;
await sendEmail({
to: vendor.email,
@@ -3472,9 +3472,8 @@ export async function getQMManagers() { .where(
and(
eq(users.isActive, true),
- ne(users.domain, "partners")
- // ilike(users.deptName, "%품질경영팀(%")
- // 테스트 간 임시제거 후 추가 예정(1103)
+ ne(users.domain, "partners"),
+ ilike(users.deptName, "%품질경영팀(%")
)
)
.orderBy(users.name);
diff --git a/lib/rfq-last/table/rfq-table-columns.tsx b/lib/rfq-last/table/rfq-table-columns.tsx index 6976e1c5..62f14579 100644 --- a/lib/rfq-last/table/rfq-table-columns.tsx +++ b/lib/rfq-last/table/rfq-table-columns.tsx @@ -337,7 +337,8 @@ export function getRfqColumns({ cell: ({ row }) => { const received = row.original.quotationReceivedCount || 0; const total = row.original.vendorCount || 0; - return `${received}/${total}`; + // return `${received}/${total}`; + return <Badge variant="outline">{received}</Badge>; }, size: 100, }, @@ -698,7 +699,8 @@ export function getRfqColumns({ cell: ({ row }) => { const received = row.original.quotationReceivedCount || 0; const total = row.original.vendorCount || 0; - return `${received}/${total}`; + // return `${received}/${total}`; + return <Badge variant="outline">{received}</Badge>; }, size: 100, }, diff --git a/lib/rfq-last/table/rfq-table-toolbar-actions.tsx b/lib/rfq-last/table/rfq-table-toolbar-actions.tsx index a6dc1ad4..7216c4d0 100644 --- a/lib/rfq-last/table/rfq-table-toolbar-actions.tsx +++ b/lib/rfq-last/table/rfq-table-toolbar-actions.tsx @@ -216,7 +216,7 @@ export function RfqTableToolbarActions<TData>({ )} </> )} - <Button + {/* <Button variant="outline" size="sm" className="flex items-center gap-2" @@ -224,7 +224,7 @@ export function RfqTableToolbarActions<TData>({ > <FileDown className="h-4 w-4" /> 엑셀 다운로드 - </Button> + </Button> */} </div> diff --git a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx index 266ba39b..281316eb 100644 --- a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx +++ b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx @@ -406,7 +406,7 @@ export default function QuotationItemsTable({ prItems, decimalPlaces = 2 }: Quot // 저장 버튼 클릭 핸들러 const handleSaveDetail = () => { - setValue(`quotationItems.${index}.deviationReason`, localDeviationReason) + setValue(`quotationItems.${index}.deviationReason`, localTechnicalCompliance ? "" : localDeviationReason) setValue(`quotationItems.${index}.itemRemark`, localItemRemark) setValue(`quotationItems.${index}.technicalCompliance`, localTechnicalCompliance) setValue(`quotationItems.${index}.alternativeProposal`, localAlternativeProposal) @@ -430,7 +430,7 @@ export default function QuotationItemsTable({ prItems, decimalPlaces = 2 }: Quot <div className="space-y-4"> {/* PR 아이템 정보 */} - <Card> + {/* <Card> <CardHeader className="pb-3"> <CardTitle className="text-base">PR 아이템 정보</CardTitle> </CardHeader> @@ -466,7 +466,7 @@ export default function QuotationItemsTable({ prItems, decimalPlaces = 2 }: Quot </div> )} </CardContent> - </Card> + </Card> */} {/* 제조사 정보 */} {/* <Card> @@ -534,17 +534,19 @@ export default function QuotationItemsTable({ prItems, decimalPlaces = 2 }: Quot /> </div> )} - - <div className="space-y-2"> - <Label htmlFor={`deviationReason-${index}`}>편차 사유</Label> - <Textarea - id={`deviationReason-${index}`} - value={localDeviationReason} - onChange={(e) => setLocalDeviationReason(e.target.value)} - placeholder="요구사항과 다른 부분이 있는 경우 사유를 입력하세요" - className="min-h-[80px]" - /> - </div> + + {!localTechnicalCompliance && ( + <div className="space-y-2"> + <Label htmlFor={`deviationReason-${index}`}>편차 사유</Label> + <Textarea + id={`deviationReason-${index}`} + value={localDeviationReason} + onChange={(e) => setLocalDeviationReason(e.target.value)} + placeholder="요구사항과 다른 부분이 있는 경우 사유를 입력하세요" + className="min-h-[80px]" + /> + </div> + )} </CardContent> </Card> diff --git a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx index b7e67881..abd2b516 100644 --- a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx +++ b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx @@ -466,6 +466,11 @@ export default function VendorResponseEditor({ <form onSubmit={(e) => { e.preventDefault() // 기본 submit 동작 방지 handleFormSubmit(false) + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() // 엔터 키로 인한 폼 제출 방지 + } }}> <div className="space-y-6"> {/* 헤더 정보 */} diff --git a/lib/rfq-last/vendor/vendor-detail-dialog.tsx b/lib/rfq-last/vendor/vendor-detail-dialog.tsx index 181e33c8..9c112efa 100644 --- a/lib/rfq-last/vendor/vendor-detail-dialog.tsx +++ b/lib/rfq-last/vendor/vendor-detail-dialog.tsx @@ -93,14 +93,14 @@ const getStatusConfig = (status: string) => { icon: <AlertCircle className="h-4 w-4" />, color: "text-orange-600", bgColor: "bg-orange-50", - variant: "warning" as const, + variant: "secondary" as const, }; case "최종확정": return { icon: <Shield className="h-4 w-4" />, color: "text-indigo-600", bgColor: "bg-indigo-50", - variant: "success" as const, + variant: "default" as const, }; case "취소": return { @@ -183,11 +183,10 @@ export function VendorResponseDetailDialog({ <div className="flex-1 overflow-y-auto px-6 "> <Tabs defaultValue="overview" className="mb-2"> - <TabsList className="grid w-full grid-cols-4"> + <TabsList className="grid w-full grid-cols-3"> <TabsTrigger value="overview">개요</TabsTrigger> <TabsTrigger value="quotation">견적정보</TabsTrigger> <TabsTrigger value="items">품목상세</TabsTrigger> - <TabsTrigger value="attachments">첨부파일</TabsTrigger> </TabsList> {/* 개요 탭 */} @@ -775,7 +774,7 @@ export function VendorResponseDetailDialog({ </TabsContent> {/* 첨부파일 탭 */} - <TabsContent value="attachments" className="space-y-4"> + {/* <TabsContent value="attachments" className="space-y-4"> {attachments.length > 0 ? ( <Card> <CardHeader> @@ -802,16 +801,6 @@ export function VendorResponseDetailDialog({ </div> </div> <div className="flex items-center gap-2"> - {/* <Button - variant="ghost" - size="sm" - onClick={() => { - // 파일 미리보기 로직 - console.log("Preview file:", file.filePath); - }} - > - <Eye className="h-4 w-4" /> - </Button> */} <Button variant="ghost" size="sm" @@ -837,7 +826,7 @@ export function VendorResponseDetailDialog({ </CardContent> </Card> )} - </TabsContent> + </TabsContent> */} </Tabs> </div> diff --git a/lib/users/auth/partners-auth.ts b/lib/users/auth/partners-auth.ts index ac0dec08..b530b608 100644 --- a/lib/users/auth/partners-auth.ts +++ b/lib/users/auth/partners-auth.ts @@ -251,7 +251,7 @@ export async function validateResetTokenAction( } if (!resetToken[0].isActive) { - return { valid: false, error: '이미 사용된 토큰입니다' }; + return { valid: false, error: '비밀번호 설정을 이미 완료하였습니다.' }; } if (resetToken[0].expiresAt < new Date()) { diff --git a/lib/vendor-investigation/table/investigation-result-sheet.tsx b/lib/vendor-investigation/table/investigation-result-sheet.tsx index 36000333..740b837e 100644 --- a/lib/vendor-investigation/table/investigation-result-sheet.tsx +++ b/lib/vendor-investigation/table/investigation-result-sheet.tsx @@ -129,10 +129,11 @@ export function InvestigationResultSheet({ defaultValues: { investigationId: investigation?.investigationId ?? 0, completedAt: investigation?.completedAt ?? undefined, + requestedAt: investigation?.requestedAt ?? undefined, // 날짜 검증을 위해 추가 evaluationScore: investigation?.evaluationScore ?? undefined, evaluationResult: investigation?.evaluationResult ?? undefined, investigationNotes: investigation?.investigationNotes ?? "", - attachments: undefined, + attachments: [], }, }) @@ -175,10 +176,11 @@ export function InvestigationResultSheet({ form.reset({ investigationId: investigation.investigationId, completedAt: investigation.completedAt ?? undefined, + requestedAt: investigation.requestedAt ?? undefined, // 날짜 검증을 위해 추가 evaluationScore: investigation.evaluationScore ?? undefined, evaluationResult: investigation.evaluationResult ?? undefined, investigationNotes: investigation.investigationNotes ?? "", - attachments: undefined, + attachments: [], }) // 기존 첨부파일 로드 @@ -240,9 +242,9 @@ export function InvestigationResultSheet({ // 선택된 파일에서 특정 파일 제거 const handleRemoveSelectedFile = (indexToRemove: number) => { - const currentFiles = form.getValues("attachments") || [] + const currentFiles = (form.getValues("attachments") as File[]) || [] const updatedFiles = currentFiles.filter((_: File, index: number) => index !== indexToRemove) - form.setValue("attachments", updatedFiles.length > 0 ? updatedFiles : undefined) + form.setValue("attachments", updatedFiles as any) if (updatedFiles.length === 0) { toast.success("모든 선택된 파일이 제거되었습니다.") @@ -326,7 +328,6 @@ export function InvestigationResultSheet({ name="attachments" render={({ field: { onChange, ...field } }) => ( <FormItem> - <FormLabel>{config.label}</FormLabel> <FormControl> <Dropzone onDrop={(acceptedFiles, rejectedFiles) => { @@ -346,7 +347,7 @@ export function InvestigationResultSheet({ if (acceptedFiles.length > 0) { // 기존 파일들과 새로 선택된 파일들을 합치기 - const currentFiles = form.getValues("attachments") || [] + const currentFiles = (form.getValues("attachments") as File[]) || [] const newFiles = [...currentFiles, ...acceptedFiles] onChange(newFiles) toast.success(`${acceptedFiles.length}개 파일이 추가되었습니다.`) @@ -513,13 +514,23 @@ export function InvestigationResultSheet({ } } - // 2) 파일이 있으면 업로드 - if (values.attachments && values.attachments.length > 0) { + // 2) 첨부파일 검증 (필수: 기존 첨부파일 + 새 파일 합계 최소 1개) + const newFilesCount = values.attachments?.length || 0 + const existingFilesCount = existingAttachments.length + const totalFilesCount = newFilesCount + existingFilesCount + + if (totalFilesCount === 0) { + toast.error("최소 1개의 첨부파일이 필요합니다.") + return + } + + // 새 파일이 있는 경우에만 업로드 진행 + if (newFilesCount > 0) { setUploadingFiles(true) try { await uploadFiles(values.attachments, values.investigationId) - toast.success(`실사 결과와 ${values.attachments.length}개 파일이 업데이트되었습니다!`) + toast.success(`실사 결과와 ${newFilesCount}개 파일이 업데이트되었습니다!`) // 첨부파일 목록 새로고침 loadExistingAttachments(values.investigationId) @@ -530,6 +541,7 @@ export function InvestigationResultSheet({ setUploadingFiles(false) } } else { + // 기존 첨부파일만 있는 경우 toast.success("실사 결과가 업데이트되었습니다!") } @@ -668,7 +680,14 @@ export function InvestigationResultSheet({ if (result === "APPROVED") return <span className="text-green-600">합격 (승인)</span> if (result === "SUPPLEMENT") return <span className="text-yellow-600">보완 필요 (방법 선택)</span> if (result === "SUPPLEMENT_REINSPECT") return <span className="text-yellow-600">보완 필요 - 재실사</span> - if (result === "SUPPLEMENT_DOCUMENT") return <span className="text-yellow-600">보완 필요 - 자료제출</span> + if (result === "SUPPLEMENT_DOCUMENT") return ( + <div className="flex flex-col gap-1"> + <span className="text-yellow-600">보완 필요 - 자료제출</span> + <span className="text-xs text-muted-foreground font-normal"> + 💡 저장 시 협력업체에 자동으로 보완 요청 메일이 발송됩니다. + </span> + </div> + ) if (result === "REJECTED") return <span className="text-destructive">불합격</span> return <span className="text-muted-foreground">-</span> })()} @@ -711,8 +730,14 @@ export function InvestigationResultSheet({ )} /> - {/* 파일 첨부 섹션 */} - {renderFileUploadSection()} + {/* 파일 첨부 섹션 */} + <div className="space-y-2"> + <FormLabel>실사 관련 첨부파일<span className="text-red-500 ml-1">*</span></FormLabel> + <div className="text-sm text-muted-foreground"> + 실사 결과 입력 시 최소 1개의 첨부파일이 필요합니다. + </div> + {renderFileUploadSection()} + </div> </form> </Form> </div> diff --git a/lib/vendor-investigation/validations.ts b/lib/vendor-investigation/validations.ts index 891ef178..84361ef9 100644 --- a/lib/vendor-investigation/validations.ts +++ b/lib/vendor-investigation/validations.ts @@ -98,6 +98,20 @@ export const updateVendorInvestigationProgressSchema = z message: "실사 수행 예정일은 필수입니다.", }) } + + // 날짜 순서 검증: forecastedAt과 confirmedAt 간의 관계 검증 + if (data.forecastedAt && data.confirmedAt) { + const forecastedDate = new Date(data.forecastedAt); + const confirmedDate = new Date(data.confirmedAt); + + if (confirmedDate < forecastedDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["confirmedAt"], + message: "실사 계획 확정일은 실사 수행 예정일보다 과거일 수 없습니다.", + }); + } + } }) export type UpdateVendorInvestigationProgressSchema = z.infer<typeof updateVendorInvestigationProgressSchema> @@ -114,15 +128,33 @@ export const updateVendorInvestigationResultSchema = z.object({ z.string().transform((str) => str ? new Date(str) : undefined) ]), + // 실사의뢰일 (날짜 검증을 위해 추가) + requestedAt: z.union([ + z.date(), + z.string().transform((str) => str ? new Date(str) : undefined) + ]).optional(), + evaluationScore: z.number() .int("평가 점수는 정수여야 합니다.") .min(0, "평가 점수는 0점 이상이어야 합니다.") .max(100, "평가 점수는 100점 이하여야 합니다."), evaluationResult: z.enum(["APPROVED", "SUPPLEMENT", "SUPPLEMENT_REINSPECT", "SUPPLEMENT_DOCUMENT", "REJECTED", "RESULT_SENT"]), investigationNotes: z.string().max(1000, "QM 의견은 1000자 이내로 입력해주세요.").optional(), - attachments: z.any({ - required_error: "첨부파일은 필수입니다." - }), + attachments: z.array(z.any()).min(1, "최소 1개의 첨부파일이 필요합니다."), +}).superRefine((data, ctx) => { + // 날짜 검증: 실제 실사일이 실사의뢰일보다 과거가 되지 않도록 검증 + if (data.requestedAt && data.completedAt) { + const requestedDate = new Date(data.requestedAt); + const completedDate = new Date(data.completedAt); + + if (completedDate < requestedDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["completedAt"], + message: "실제 실사일은 실사의뢰일보다 과거일 수 없습니다.", + }); + } + } }) export type UpdateVendorInvestigationResultSchema = z.infer<typeof updateVendorInvestigationResultSchema> @@ -137,28 +169,28 @@ export const updateVendorInvestigationSchema = z.object({ }), investigationAddress: z.string().optional(), investigationMethod: z.enum(["PURCHASE_SELF_EVAL", "DOCUMENT_EVAL", "PRODUCT_INSPECTION", "SITE_VISIT_EVAL"]).optional(), - + // 날짜 필드들 forecastedAt: z.union([ z.date(), z.string().transform((str) => str ? new Date(str) : undefined) ]).optional(), - + requestedAt: z.union([ z.date(), z.string().transform((str) => str ? new Date(str) : undefined) ]).optional(), - + confirmedAt: z.union([ z.date(), z.string().transform((str) => str ? new Date(str) : undefined) ]).optional(), - + completedAt: z.union([ z.date(), z.string().transform((str) => str ? new Date(str) : undefined) ]).optional(), - + evaluationScore: z.number() .int("평가 점수는 정수여야 합니다.") .min(0, "평가 점수는 0점 이상이어야 합니다.") @@ -167,6 +199,47 @@ export const updateVendorInvestigationSchema = z.object({ evaluationResult: z.enum(["APPROVED", "SUPPLEMENT", "SUPPLEMENT_REINSPECT", "SUPPLEMENT_DOCUMENT", "REJECTED", "RESULT_SENT"]).optional(), investigationNotes: z.string().max(1000, "QM 의견은 1000자 이내로 입력해주세요.").optional(), attachments: z.any().optional(), // File 업로드를 위한 필드 +}).superRefine((data, ctx) => { + // 날짜 검증: 실사의뢰일(requestedAt)이 있는 경우 다른 날짜들이 실사의뢰일보다 과거가 되지 않도록 검증 + if (data.requestedAt) { + const requestedDate = new Date(data.requestedAt); + + // 실사수행/계획일(forecastedAt) 검증 + if (data.forecastedAt) { + const forecastedDate = new Date(data.forecastedAt); + if (forecastedDate < requestedDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["forecastedAt"], + message: "실사 수행 예정일은 실사의뢰일보다 과거일 수 없습니다.", + }); + } + } + + // 실사 계획 확정일(confirmedAt) 검증 + if (data.confirmedAt) { + const confirmedDate = new Date(data.confirmedAt); + if (confirmedDate < requestedDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["confirmedAt"], + message: "실사 계획 확정일은 실사의뢰일보다 과거일 수 없습니다.", + }); + } + } + + // 실제 실사일(completedAt) 검증 + if (data.completedAt) { + const completedDate = new Date(data.completedAt); + if (completedDate < requestedDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["completedAt"], + message: "실제 실사일은 실사의뢰일보다 과거일 수 없습니다.", + }); + } + } + } }) export type UpdateVendorInvestigationSchema = z.infer<typeof updateVendorInvestigationSchema>
\ No newline at end of file diff --git a/lib/vendor-regular-registrations/repository.ts b/lib/vendor-regular-registrations/repository.ts index f2a33cda..314afb6c 100644 --- a/lib/vendor-regular-registrations/repository.ts +++ b/lib/vendor-regular-registrations/repository.ts @@ -170,7 +170,8 @@ export async function getVendorRegularRegistrations( const shouldUpdateStatus = allDocumentsSubmitted && allContractsCompleted && safetyQualificationCompleted && additionalInfoCompleted;
// 현재 상태가 조건충족이 아닌데 모든 조건이 충족되면 상태 업데이트
- if (shouldUpdateStatus && registration.status !== "approval_ready") {
+ // 단, 결재진행중(pending_approval) 또는 등록요청완료(registration_completed), 등록실패(registration_failed) 상태인 경우 무시
+ if (shouldUpdateStatus && registration.status !== "approval_ready" && registration.status !== "pending_approval" && registration.status !== "registration_completed" && registration.status !== "registration_failed") {
// 비동기 업데이트 (백그라운드에서 실행)
updateVendorRegularRegistration(registration.id, {
status: "approval_ready"
diff --git a/lib/vendors/table/request-vendor-investigate-dialog.tsx b/lib/vendors/table/request-vendor-investigate-dialog.tsx deleted file mode 100644 index a0d84128..00000000 --- a/lib/vendors/table/request-vendor-investigate-dialog.tsx +++ /dev/null @@ -1,345 +0,0 @@ -"use client" - -import * as React from "react" -import { type Row } from "@tanstack/react-table" -import { Loader, Check, SendHorizonal, AlertCircle, AlertTriangle } from "lucide-react" -import { toast } from "sonner" - -import { useMediaQuery } from "@/hooks/use-media-query" -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerDescription, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "@/components/ui/drawer" -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion" -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { ScrollArea } from "@/components/ui/scroll-area" -import { Badge } from "@/components/ui/badge" -import { Separator } from "@/components/ui/separator" - -import { Vendor } from "@/db/schema/vendors" -import { requestInvestigateVendors, getExistingInvestigationsForVendors } from "@/lib/vendor-investigation/service" -import { useSession } from "next-auth/react" -import { formatDate } from "@/lib/utils" - -interface ApprovalVendorDialogProps - extends React.ComponentPropsWithoutRef<typeof Dialog> { - vendors: Row<Vendor>["original"][] - showTrigger?: boolean - onSuccess?: () => void -} - -// Helper function to get status badge variant and text -function getStatusBadge(status: string) { - switch (status) { - case "REQUESTED": - return { variant: "secondary", text: "Requested" } - case "SCHEDULED": - return { variant: "warning", text: "Scheduled" } - case "IN_PROGRESS": - return { variant: "default", text: "In Progress" } - case "COMPLETED": - return { variant: "success", text: "Completed" } - case "CANCELLED": - return { variant: "destructive", text: "Cancelled" } - default: - return { variant: "outline", text: status } - } -} - -export function RequestVendorsInvestigateDialog({ - vendors, - showTrigger = true, - onSuccess, - ...props -}: ApprovalVendorDialogProps) { - const [isApprovePending, startApproveTransition] = React.useTransition() - const [isLoading, setIsLoading] = React.useState(true) - const [existingInvestigations, setExistingInvestigations] = React.useState<any[]>([]) - const isDesktop = useMediaQuery("(min-width: 640px)") - const { data: session } = useSession() - - // Fetch existing investigations when dialog opens - React.useEffect(() => { - if (vendors.length > 0) { - setIsLoading(true) - const fetchExistingInvestigations = async () => { - try { - const vendorIds = vendors.map(vendor => vendor.id) - const result = await getExistingInvestigationsForVendors(vendorIds) - setExistingInvestigations(result) - } catch (error) { - console.error("Failed to fetch existing investigations:", error) - toast.error("Failed to fetch existing investigations") - } finally { - setIsLoading(false) - } - } - - fetchExistingInvestigations() - } - }, [vendors]) - - // Group vendors by investigation status - const vendorsWithInvestigations = React.useMemo(() => { - if (!existingInvestigations.length) return { withInvestigations: [], withoutInvestigations: vendors } - - const vendorMap = new Map(vendors.map(v => [v.id, v])) - const withInvestigations: Array<{ vendor: typeof vendors[0], investigation: any }> = [] - - // Find vendors with existing investigations - existingInvestigations.forEach(inv => { - const vendor = vendorMap.get(inv.vendorId) - if (vendor) { - withInvestigations.push({ vendor, investigation: inv }) - vendorMap.delete(inv.vendorId) - } - }) - - // Remaining vendors don't have investigations - const withoutInvestigations = Array.from(vendorMap.values()) - - return { withInvestigations, withoutInvestigations } - }, [vendors, existingInvestigations]) - - function onApprove() { - if (!session?.user?.id) { - toast.error("사용자 인증 정보를 찾을 수 없습니다.") - return - } - - // Only request investigations for vendors without existing ones - const vendorsToRequest = vendorsWithInvestigations.withoutInvestigations - - if (vendorsToRequest.length === 0) { - toast.info("모든 선택된 업체에 이미 실사 요청이 있습니다.") - props.onOpenChange?.(false) - return - } - - startApproveTransition(async () => { - const { error } = await requestInvestigateVendors({ - ids: vendorsToRequest.map((vendor) => vendor.id), - userId: Number(session.user.id) - }) - - if (error) { - toast.error(error) - return - } - - props.onOpenChange?.(false) - toast.success(`${vendorsToRequest.length}개 업체에 대한 실사 요청을 보냈습니다.`) - onSuccess?.() - }) - } - - const renderContent = () => { - return ( - <> - <div className="space-y-4"> - {isLoading ? ( - <div className="flex items-center justify-center py-4"> - <Loader className="size-6 animate-spin text-muted-foreground" /> - </div> - ) : ( - <> - {vendorsWithInvestigations.withInvestigations.length > 0 && ( - <Alert> - <AlertTriangle className="h-4 w-4" /> - <AlertTitle>기존 실사 요청 정보가 있습니다</AlertTitle> - <AlertDescription> - 선택한 {vendors.length}개 업체 중 {vendorsWithInvestigations.withInvestigations.length}개 업체에 대한 - 기존 실사 요청이 있습니다. 새로운 요청은 기존 데이터가 없는 업체에만 적용됩니다. - </AlertDescription> - </Alert> - )} - - {vendorsWithInvestigations.withInvestigations.length > 0 && ( - <Accordion type="single" collapsible className="w-full"> - <AccordionItem value="existing-investigations"> - <AccordionTrigger className="font-medium"> - 기존 실사 요청 ({vendorsWithInvestigations.withInvestigations.length}) - </AccordionTrigger> - <AccordionContent> - <ScrollArea className="max-h-[200px]"> - <Table> - <TableHeader> - <TableRow> - <TableHead>업체명</TableHead> - <TableHead>상태</TableHead> - <TableHead>요청일</TableHead> - <TableHead>예정 일정</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {vendorsWithInvestigations.withInvestigations.map(({ vendor, investigation }) => { - const status = getStatusBadge(investigation.investigationStatus) - return ( - <TableRow key={investigation.investigationId}> - <TableCell className="font-medium">{vendor.vendorName}</TableCell> - <TableCell> - <Badge variant={status.variant as any}>{status.text}</Badge> - </TableCell> - <TableCell>{formatDate(investigation.createdAt, "KR")}</TableCell> - <TableCell> - {investigation.scheduledStartAt - ? formatDate(investigation.scheduledStartAt, "KR") - : "미정"} - </TableCell> - </TableRow> - ) - })} - </TableBody> - </Table> - </ScrollArea> - </AccordionContent> - </AccordionItem> - </Accordion> - )} - - <div> - <h3 className="text-sm font-medium mb-2"> - 새로운 실사가 요청될 업체 ({vendorsWithInvestigations.withoutInvestigations.length}) - </h3> - {vendorsWithInvestigations.withoutInvestigations.length > 0 ? ( - <ScrollArea className="max-h-[200px]"> - <ul className="space-y-1"> - {vendorsWithInvestigations.withoutInvestigations.map((vendor) => ( - <li key={vendor.id} className="text-sm py-1 px-2 border-b"> - {vendor.vendorName} ({vendor.vendorCode || "코드 없음"}) - </li> - ))} - </ul> - </ScrollArea> - ) : ( - <p className="text-sm text-muted-foreground py-2"> - 모든 선택된 업체에 이미 실사 요청이 있습니다. - </p> - )} - </div> - </> - )} - </div> - </> - ) - } - - if (isDesktop) { - return ( - <Dialog {...props}> - {showTrigger ? ( - <DialogTrigger asChild> - <Button variant="outline" size="sm" className="gap-2"> - <SendHorizonal className="size-4" aria-hidden="true" /> - 실사 요청 ({vendors.length}) - </Button> - </DialogTrigger> - ) : null} - <DialogContent className="max-w-md"> - <DialogHeader> - <DialogTitle>Confirm Vendor Investigation Request</DialogTitle> - <DialogDescription> - 선택한 {vendors.length}개 업체에 대한 실사 요청을 확인합니다. - 요청 후 협력업체실사담당자에게 알림이 전송됩니다. - </DialogDescription> - </DialogHeader> - - {renderContent()} - - <DialogFooter className="gap-2 sm:space-x-0 mt-4"> - <DialogClose asChild> - <Button variant="outline">취소</Button> - </DialogClose> - <Button - aria-label="Request selected vendors" - variant="default" - onClick={onApprove} - disabled={isApprovePending || isLoading || vendorsWithInvestigations.withoutInvestigations.length === 0} - > - {isApprovePending && ( - <Loader - className="mr-2 size-4 animate-spin" - aria-hidden="true" - /> - )} - 요청하기 - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ) - } - - return ( - <Drawer {...props}> - {showTrigger ? ( - <DrawerTrigger asChild> - <Button variant="outline" size="sm" className="gap-2"> - <Check className="size-4" aria-hidden="true" /> - Investigation Request ({vendors.length}) - </Button> - </DrawerTrigger> - ) : null} - <DrawerContent> - <DrawerHeader> - <DrawerTitle>Confirm Vendor Investigation</DrawerTitle> - <DrawerDescription> - 선택한 {vendors.length}개 업체에 대한 실사 요청을 확인합니다. - 요청 후 협력업체실사담당자에게 알림이 전송됩니다. - </DrawerDescription> - </DrawerHeader> - - <div className="px-4"> - {renderContent()} - </div> - - <DrawerFooter className="gap-2 sm:space-x-0 mt-4"> - <DrawerClose asChild> - <Button variant="outline">취소</Button> - </DrawerClose> - <Button - aria-label="Request selected vendors" - variant="default" - onClick={onApprove} - disabled={isApprovePending || isLoading || vendorsWithInvestigations.withoutInvestigations.length === 0} - > - {isApprovePending && ( - <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> - )} - 요청하기 - </Button> - </DrawerFooter> - </DrawerContent> - </Drawer> - ) -}
\ No newline at end of file diff --git a/lib/vendors/table/vendors-table-toolbar-actions.tsx b/lib/vendors/table/vendors-table-toolbar-actions.tsx index def46168..a2611b64 100644 --- a/lib/vendors/table/vendors-table-toolbar-actions.tsx +++ b/lib/vendors/table/vendors-table-toolbar-actions.tsx @@ -20,7 +20,6 @@ import { RequestPQVendorsDialog } from "./request-vendor-pg-dialog" import { RequestPQDialog } from "./request-pq-dialog" import { RequestProjectPQDialog } from "./request-project-pq-dialog" import { SendVendorsDialog } from "./send-vendor-dialog" -import { RequestVendorsInvestigateDialog } from "./request-vendor-investigate-dialog" import { RequestInfoDialog } from "./request-additional-Info-dialog" import { RequestContractDialog } from "./request-basicContract-dialog" import { exportVendorsWithRelatedData } from "./vendor-all-export" |
