diff options
Diffstat (limited to 'components')
| -rw-r--r-- | components/additional-info/join-form.tsx | 60 | ||||
| -rw-r--r-- | components/pq-input/pq-input-tabs.tsx | 4 | ||||
| -rw-r--r-- | components/pq-input/pq-review-wrapper.tsx | 326 | ||||
| -rw-r--r-- | components/signup/join-form.tsx | 152 | ||||
| -rw-r--r-- | components/vendor-regular-registrations/document-status-dialog.tsx | 19 |
5 files changed, 320 insertions, 241 deletions
diff --git a/components/additional-info/join-form.tsx b/components/additional-info/join-form.tsx index afe38841..1642962f 100644 --- a/components/additional-info/join-form.tsx +++ b/components/additional-info/join-form.tsx @@ -223,6 +223,8 @@ export function InfoForm() { }) const isFormValid = form.formState.isValid + const watchedCountry = form.watch("country") + const isDomesticVendor = watchedCountry === "KR" // Field array for contacts const { fields: contactFields, append: addContact, remove: removeContact, replace: replaceContacts } = @@ -369,6 +371,29 @@ export function InfoForm() { fetchVendorData() }, [companyId, form, replaceContacts]) + // 도로명주소 검색 결과 수신 (내자 벤더만) + React.useEffect(() => { + if (!isDomesticVendor) return + + const handleMessage = (event: MessageEvent) => { + if (!event.data || event.data.type !== "JUSO_SELECTED") return + const { zipNo, roadAddrPart1, roadAddrPart2, addrDetail } = event.data.payload || {} + const road = [roadAddrPart1, roadAddrPart2].filter(Boolean).join(" ").trim() + + form.setValue("postalCode", zipNo || form.getValues("postalCode") || "", { shouldDirty: true }) + form.setValue("address", road || form.getValues("address") || "", { shouldDirty: true }) + form.setValue("addressDetail", addrDetail || form.getValues("addressDetail") || "", { shouldDirty: true }) + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, [isDomesticVendor, form]) + + const handleJusoSearch = () => { + if (!isDomesticVendor) return + window.open("/api/juso", "jusoSearch", "width=570,height=420,scrollbars=yes,resizable=yes") + } + // 컴포넌트 언마운트 시 미리보기 URL 정리 (blob URL만) React.useEffect(() => { return () => { @@ -1224,11 +1249,29 @@ export function InfoForm() { name="address" render={({ field }) => ( <FormItem> - <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500"> - 주소 - </FormLabel> + <div className="flex items-center justify-between gap-2"> + <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500"> + 주소 + </FormLabel> + {isDomesticVendor && ( + <Button + type="button" + variant="secondary" + size="sm" + onClick={handleJusoSearch} + disabled={isSubmitting} + > + 주소 검색 + </Button> + )} + </div> <FormControl> - <Input {...field} disabled={isSubmitting} /> + <Input + {...field} + disabled={isSubmitting} + readOnly={isDomesticVendor} + className={cn(isDomesticVendor && "bg-muted text-muted-foreground")} + /> </FormControl> <FormMessage /> </FormItem> @@ -1258,7 +1301,13 @@ export function InfoForm() { <FormItem> <FormLabel>우편번호</FormLabel> <FormControl> - <Input {...field} disabled={isSubmitting} placeholder="우편번호를 입력해주세요" /> + <Input + {...field} + disabled={isSubmitting} + readOnly={isDomesticVendor} + className={cn(isDomesticVendor && "bg-muted text-muted-foreground")} + placeholder="우편번호를 입력해주세요" + /> </FormControl> <FormMessage /> </FormItem> @@ -1960,7 +2009,6 @@ export function InfoForm() { assignedDepartment: registrationData.registration?.assignedDepartment, assignedUser: registrationData.registration?.assignedUser, remarks: registrationData.registration?.remarks, - safetyQualificationContent: registrationData.registration?.safetyQualificationContent || null, gtcSkipped: registrationData.registration?.gtcSkipped || false, additionalInfo: registrationData.additionalInfo, documentSubmissions: registrationData.documentStatus, diff --git a/components/pq-input/pq-input-tabs.tsx b/components/pq-input/pq-input-tabs.tsx index 6c9a1254..6ffd637a 100644 --- a/components/pq-input/pq-input-tabs.tsx +++ b/components/pq-input/pq-input-tabs.tsx @@ -651,8 +651,8 @@ export function PQInputTabs({ if (result.ok) { toast({ - title: "PQ Submitted", - description: "Your PQ information has been submitted successfully", + title: "PQ 제출 완료", + description: "PQ 정보가 성공적으로 제출되었습니다", }); // 제출 후 PQ 목록 페이지로 리디렉션 window.location.href = "/partners/pq_new"; diff --git a/components/pq-input/pq-review-wrapper.tsx b/components/pq-input/pq-review-wrapper.tsx index efb078e0..ac9629cb 100644 --- a/components/pq-input/pq-review-wrapper.tsx +++ b/components/pq-input/pq-review-wrapper.tsx @@ -23,7 +23,7 @@ import { import { useToast } from "@/hooks/use-toast" import { CheckCircle, AlertCircle, Paperclip, Square, Download } from "lucide-react" import { PQGroupData } from "@/lib/pq/service" -import { approvePQAction, rejectPQAction, updateSHICommentAction, approveQMReviewAction, rejectQMReviewAction, requestPqSupplementAction } from "@/lib/pq/service" +import { approvePQAction, rejectPQAction, updateSHICommentAction, approveQMReviewAction, rejectQMReviewAction, requestPqSupplementAction, approveSafetyPQAction, rejectSafetyPQAction } from "@/lib/pq/service" import { FileList, FileListHeader, FileListInfo, FileListItem, FileListName, FileListDescription, FileListAction, FileListIcon } from "@/components/ui/file-list" // import * as ExcelJS from 'exceljs'; // import { saveAs } from "file-saver"; @@ -62,15 +62,20 @@ export function PQReviewWrapper({ }: PQReviewWrapperProps) { const router = useRouter() const { toast } = useToast() + const [isSafetyApproving, setIsSafetyApproving] = React.useState(false) + const [isSafetyRejecting, setIsSafetyRejecting] = React.useState(false) const [isApproving, setIsApproving] = React.useState(false) const [isRejecting, setIsRejecting] = React.useState(false) const [isQMApproving, setIsQMApproving] = React.useState(false) const [isQMRejecting, setIsQMRejecting] = React.useState(false) + const [showSafetyApproveDialog, setShowSafetyApproveDialog] = React.useState(false) + const [showSafetyRejectDialog, setShowSafetyRejectDialog] = React.useState(false) const [showApproveDialog, setShowApproveDialog] = React.useState(false) const [showRejectDialog, setShowRejectDialog] = React.useState(false) const [showQMApproveDialog, setShowQMApproveDialog] = React.useState(false) const [showQMRejectDialog, setShowQMRejectDialog] = React.useState(false) const [showSupplementDialog, setShowSupplementDialog] = React.useState(false) + const [safetyRejectReason, setSafetyRejectReason] = React.useState("") const [rejectReason, setRejectReason] = React.useState("") const [qmRejectReason, setQmRejectReason] = React.useState("") const [supplementComment, setSupplementComment] = React.useState("") @@ -140,6 +145,87 @@ export function PQReviewWrapper({ setShiComments(initialComments) }, [pqData]) + // 안전 PQ 승인 처리 + const handleSafetyApprove = async () => { + try { + setIsSafetyApproving(true) + const result = await approveSafetyPQAction({ + pqSubmissionId: pqSubmission.id, + vendorId: vendorId, + }) + + if (result.ok) { + toast({ + title: "안전 PQ 승인 완료", + description: "안전 검토가 승인되었습니다.", + }) + router.refresh() + } else { + toast({ + title: "안전 승인 실패", + description: result.error || "안전 PQ 승인 중 오류가 발생했습니다.", + variant: "destructive", + }) + } + } catch (error) { + console.error("안전 PQ 승인 오류:", error) + toast({ + title: "안전 승인 실패", + description: "안전 PQ 승인 중 오류가 발생했습니다.", + variant: "destructive", + }) + } finally { + setIsSafetyApproving(false) + setShowSafetyApproveDialog(false) + } + } + + // 안전 PQ 거절 처리 + const handleSafetyReject = async () => { + if (!safetyRejectReason.trim()) { + toast({ + title: "거절 사유 필요", + description: "안전 거절 사유를 입력해주세요.", + variant: "destructive", + }) + return + } + + try { + setIsSafetyRejecting(true) + const result = await rejectSafetyPQAction({ + pqSubmissionId: pqSubmission.id, + vendorId: vendorId, + rejectReason: safetyRejectReason, + }) + + if (result.ok) { + toast({ + title: "안전 PQ 거절 완료", + description: "안전 검토에서 거절되었습니다.", + }) + router.refresh() + } else { + toast({ + title: "안전 거절 실패", + description: result.error || "안전 PQ 거절 중 오류가 발생했습니다.", + variant: "destructive", + }) + } + } catch (error) { + console.error("안전 PQ 거절 오류:", error) + toast({ + title: "안전 거절 실패", + description: "안전 PQ 거절 중 오류가 발생했습니다.", + variant: "destructive", + }) + } finally { + setSafetyRejectReason("") + setIsSafetyRejecting(false) + setShowSafetyRejectDialog(false) + } + } + // PQ 승인 처리 const handleApprove = async () => { try { @@ -327,141 +413,6 @@ export function PQReviewWrapper({ } } - // // Excel export 처리 - // const handleExportToExcel = async () => { - // try { - // setIsExporting(true) - - // // 워크북 생성 - // const workbook = new ExcelJS.Workbook() - // workbook.creator = 'PQ Management System' - // workbook.created = new Date() - - // // 메인 시트 생성 - // const worksheet = workbook.addWorksheet("PQ 항목") - - // // 헤더 정의 - // const headers = [ - // "그룹명", - // "코드", - // "체크포인트", - // "설명", - // "입력형식", - // "필수여부", - // "벤더답변", - // "SHI 코멘트", - // "벤더 답변", - // ] - - // // 헤더 추가 - // worksheet.addRow(headers) - - // // 헤더 스타일 적용 - // const headerRow = worksheet.getRow(1) - // headerRow.font = { bold: true } - // headerRow.fill = { - // type: 'pattern', - // pattern: 'solid', - // fgColor: { argb: 'FFE0E0E0' } - // } - // headerRow.alignment = { vertical: 'middle', horizontal: 'center' } - - // // 컬럼 너비 설정 - // worksheet.columns = [ - // { header: "그룹명", key: "groupName", width: 15 }, - // { header: "코드", key: "code", width: 12 }, - // { header: "체크포인트", key: "checkPoint", width: 30 }, - // { header: "설명", key: "description", width: 40 }, - // { header: "입력형식", key: "inputFormat", width: 12 }, - - // { header: "벤더답변", key: "answer", width: 30 }, - // { header: "SHI 코멘트", key: "shiComment", width: 30 }, - // { header: "벤더 답변", key: "vendorReply", width: 30 }, - // ] - - // // 데이터 추가 - // pqData.forEach(group => { - // group.items.forEach(item => { - // const rowData = [ - // group.groupName, - // item.code, - // item.checkPoint, - // item.description || "", - // item.inputFormat || "", - - // item.answer || "", - // item.shiComment || "", - // item.vendorReply || "", - // ] - // worksheet.addRow(rowData) - // }) - // }) - - // // 전체 셀에 테두리 추가 - // worksheet.eachRow((row, rowNumber) => { - // row.eachCell((cell) => { - // cell.border = { - // top: { style: 'thin' }, - // left: { style: 'thin' }, - // bottom: { style: 'thin' }, - // right: { style: 'thin' } - // } - // // 긴 텍스트는 자동 줄바꿈 - // cell.alignment = { - // vertical: 'top', - // horizontal: 'left', - // wrapText: true - // } - // }) - // }) - - // // 정보 시트 생성 - // const infoSheet = workbook.addWorksheet("정보") - // infoSheet.addRow(["벤더명", pqSubmission.vendorName]) - // if (pqSubmission.projectName) { - // infoSheet.addRow(["프로젝트명", pqSubmission.projectName]) - // } - // infoSheet.addRow(["생성일", new Date().toLocaleDateString('ko-KR')]) - // infoSheet.addRow(["총 항목 수", pqData.reduce((total, group) => total + group.items.length, 0)]) - - // // 정보 시트 스타일링 - // infoSheet.columns = [ - // { header: "항목", key: "item", width: 20 }, - // { header: "값", key: "value", width: 40 } - // ] - - // const infoHeaderRow = infoSheet.getRow(1) - // infoHeaderRow.font = { bold: true } - // infoHeaderRow.fill = { - // type: 'pattern', - // pattern: 'solid', - // fgColor: { argb: 'FFE6F3FF' } - // } - - // // 파일명 생성 - // const defaultFilename = pqSubmission.projectName - // ? `${pqSubmission.vendorName}_${pqSubmission.projectName}_PQ_${new Date().toISOString().slice(0, 10)}` - // : `${pqSubmission.vendorName}_PQ_${new Date().toISOString().slice(0, 10)}` - // const finalFilename = defaultFilename - - // // 파일 다운로드 - // const buffer = await workbook.xlsx.writeBuffer() - // const blob = new Blob([buffer], { - // type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - // }) - // saveAs(blob, `${finalFilename}.xlsx`) - // } catch (error) { - // console.error("Excel export 오류:", error) - // toast({ - // title: "내보내기 실패", - // description: "Excel 내보내기 중 오류가 발생했습니다.", - // variant: "destructive" - // }) - // } finally { - // setIsExporting(false) - // } - // } - // PQ 거부 처리 const handleReject = async () => { if (!rejectReason.trim()) { @@ -839,15 +790,42 @@ export function PQReviewWrapper({ {/* 검토 버튼 - 상태에 따라 다른 버튼 표시 */} <div className="fixed bottom-4 right-4 bg-background p-4 rounded-lg shadow-md border"> <div className="flex gap-2"> - {/* SUBMITTED 상태: 구매 담당자 승인/거절 */} + {/* SUBMITTED 상태: 안전 담당자 승인/거절 */} {pqSubmission.status === "SUBMITTED" && ( <> <Button variant="outline" + onClick={() => setShowSafetyRejectDialog(true)} + disabled={isSafetyRejecting} + > + {isSafetyRejecting ? "안전 거절 중..." : "안전 거절"} + </Button> + <Button + variant="secondary" + onClick={() => setShowSupplementDialog(true)} + disabled={isSendingSupplement} + > + 보완요청 + </Button> + <Button + variant="default" + onClick={() => setShowSafetyApproveDialog(true)} + disabled={isSafetyApproving} + > + {isSafetyApproving ? "안전 승인 중..." : "안전 승인"} + </Button> + </> + )} + + {/* SAFETY_APPROVED 상태: 구매 담당자 승인/거절 */} + {pqSubmission.status === "SAFETY_APPROVED" && ( + <> + <Button + variant="outline" onClick={() => setShowRejectDialog(true)} disabled={isRejecting} > - {isRejecting ? "거부 중..." : "거부"} + {isRejecting ? "거부 중..." : "구매 거부"} </Button> <Button variant="secondary" @@ -916,10 +894,82 @@ export function PQReviewWrapper({ <span>거절됨</span> </div> )} + + {/* SAFETY_REJECTED 상태: 안전 거절 표시 */} + {pqSubmission.status === "SAFETY_REJECTED" && ( + <div className="flex items-center gap-2 text-red-600"> + <AlertCircle className="h-4 w-4" /> + <span>안전 거절됨</span> + </div> + )} </div> </div> + {/* 안전 승인 확인 다이얼로그 */} + <Dialog open={showSafetyApproveDialog} onOpenChange={setShowSafetyApproveDialog}> + <DialogContent> + <DialogHeader> + <DialogTitle>안전 PQ 승인 확인</DialogTitle> + <DialogDescription> + {pqSubmission.vendorName || "알 수 없는 업체"}의 { + pqSubmission.type === "GENERAL" ? "일반" : + pqSubmission.type === "PROJECT" ? "프로젝트" : + pqSubmission.type === "NON_INSPECTION" ? "미실사" : "일반" + } PQ를 안전 검토에서 승인하시겠습니까? + {pqSubmission.projectId && ( + <span> 프로젝트: {pqSubmission.projectName}</span> + )} + </DialogDescription> + </DialogHeader> + <DialogFooter> + <Button variant="outline" onClick={() => setShowSafetyApproveDialog(false)}> + 취소 + </Button> + <Button onClick={handleSafetyApprove} disabled={isSafetyApproving}> + {isSafetyApproving ? "안전 승인 중..." : "안전 승인"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + + {/* 안전 거부 확인 다이얼로그 */} + <Dialog open={showSafetyRejectDialog} onOpenChange={setShowSafetyRejectDialog}> + <DialogContent> + <DialogHeader> + <DialogTitle>안전 PQ 거부</DialogTitle> + <DialogDescription> + {pqSubmission.vendorName || "알 수 없는 업체"}의 { + pqSubmission.type === "GENERAL" ? "일반" : + pqSubmission.type === "PROJECT" ? "프로젝트" : + pqSubmission.type === "NON_INSPECTION" ? "미실사" : "일반" + } PQ를 안전 검토에서 거부하는 이유를 입력해주세요. + {pqSubmission.projectId && ( + <span> 프로젝트: {pqSubmission.projectName}</span> + )} + </DialogDescription> + </DialogHeader> + <Textarea + value={safetyRejectReason} + onChange={(e) => setSafetyRejectReason(e.target.value)} + placeholder="안전 거부 사유를 입력하세요" + className="min-h-24" + /> + <DialogFooter> + <Button variant="outline" onClick={() => setShowSafetyRejectDialog(false)}> + 취소 + </Button> + <Button + variant="destructive" + onClick={handleSafetyReject} + disabled={isSafetyRejecting || !safetyRejectReason.trim()} + > + {isSafetyRejecting ? "안전 거절 중..." : "안전 거절"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + {/* 승인 확인 다이얼로그 */} <Dialog open={showApproveDialog} onOpenChange={setShowApproveDialog}> <DialogContent> diff --git a/components/signup/join-form.tsx b/components/signup/join-form.tsx index c6281b24..973fb00c 100644 --- a/components/signup/join-form.tsx +++ b/components/signup/join-form.tsx @@ -1431,6 +1431,82 @@ function CompleteVendorForm({ </div> </div> + {/* 한국 사업자 정보 */} + {data.country === "KR" && ( + <div className="rounded-md border p-6 space-y-4"> + <h4 className="text-md font-semibold">{t('koreanBusinessInfo')}</h4> + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + <div> + <label className="block text-sm font-medium mb-1"> + {t('name')} <span className="text-red-500">*</span> + </label> + <Input + value={data.representativeName} + onChange={(e) => handleInputChange('representativeName', e.target.value)} + disabled={isSubmitting} + /> + </div> + <div> + <label className="block text-sm font-medium mb-1"> + {t('representativeBirth')} <span className="text-red-500">*</span> + </label> + <Input + placeholder="YYYY-MM-DD" + value={data.representativeBirth} + onChange={(e) => handleInputChange('representativeBirth', e.target.value)} + disabled={isSubmitting} + /> + </div> + <div> + <label className="block text-sm font-medium mb-1"> + {t('representativeEmail')} <span className="text-red-500">*</span> + </label> + <Input + value={data.representativeEmail} + onChange={(e) => handleInputChange('representativeEmail', e.target.value)} + disabled={isSubmitting} + /> + </div> + <div> + <label className="block text-sm font-medium mb-1"> + {t('')} <span className="text-red-500">*</span> + </label> + <PhoneInput + value={data.representativePhone} + onChange={(value) => handleInputChange('representativePhone', value)} + countryCode="KR" + disabled={isSubmitting} + showValidation={true} + t={t} + /> + </div> + <div> + <label className="block text-sm font-medium mb-1"> + {t('corporateRegistrationNumber')} + </label> + <Input + value={data.corporateRegistrationNumber} + onChange={(e) => handleInputChange('corporateRegistrationNumber', e.target.value)} + disabled={isSubmitting} + /> + </div> + <div className="flex items-center space-x-2"> + <input + type="checkbox" + id="work-experience" + checked={data.representativeWorkExpirence} + onChange={(e) => handleInputChange('representativeWorkExpirence', e.target.checked)} + disabled={isSubmitting} + className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" + /> + <label htmlFor="work-experience" className="text-sm"> + {t('samsungWorkExperience')} + </label> + </div> + </div> + </div> + )} + {/* 담당자 정보 */} <div className="rounded-md border p-6 space-y-4"> <div className="flex items-center justify-between"> @@ -1551,82 +1627,6 @@ function CompleteVendorForm({ </div> </div> - {/* 한국 사업자 정보 */} - {data.country === "KR" && ( - <div className="rounded-md border p-6 space-y-4"> - <h4 className="text-md font-semibold">{t('koreanBusinessInfo')}</h4> - <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> - <div> - <label className="block text-sm font-medium mb-1"> - {t('representativeName')} <span className="text-red-500">*</span> - </label> - <Input - value={data.representativeName} - onChange={(e) => handleInputChange('representativeName', e.target.value)} - disabled={isSubmitting} - /> - </div> - <div> - <label className="block text-sm font-medium mb-1"> - {t('representativeBirth')} <span className="text-red-500">*</span> - </label> - <Input - placeholder="YYYY-MM-DD" - value={data.representativeBirth} - onChange={(e) => handleInputChange('representativeBirth', e.target.value)} - disabled={isSubmitting} - /> - </div> - <div> - <label className="block text-sm font-medium mb-1"> - {t('representativeEmail')} <span className="text-red-500">*</span> - </label> - <Input - value={data.representativeEmail} - onChange={(e) => handleInputChange('representativeEmail', e.target.value)} - disabled={isSubmitting} - /> - </div> - <div> - <label className="block text-sm font-medium mb-1"> - {t('representativePhone')} <span className="text-red-500">*</span> - </label> - <PhoneInput - value={data.representativePhone} - onChange={(value) => handleInputChange('representativePhone', value)} - countryCode="KR" - disabled={isSubmitting} - showValidation={true} - t={t} - /> - </div> - <div> - <label className="block text-sm font-medium mb-1"> - {t('corporateRegistrationNumber')} - </label> - <Input - value={data.corporateRegistrationNumber} - onChange={(e) => handleInputChange('corporateRegistrationNumber', e.target.value)} - disabled={isSubmitting} - /> - </div> - <div className="flex items-center space-x-2"> - <input - type="checkbox" - id="work-experience" - checked={data.representativeWorkExpirence} - onChange={(e) => handleInputChange('representativeWorkExpirence', e.target.checked)} - disabled={isSubmitting} - className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" - /> - <label htmlFor="work-experience" className="text-sm"> - {t('samsungWorkExperience')} - </label> - </div> - </div> - </div> - )} - {/* 필수 첨부 서류 */} <div className="rounded-md border p-6 space-y-6"> <h4 className="text-md font-semibold">{t('requiredDocuments')}</h4> diff --git a/components/vendor-regular-registrations/document-status-dialog.tsx b/components/vendor-regular-registrations/document-status-dialog.tsx index 02da19bf..1bd96ee8 100644 --- a/components/vendor-regular-registrations/document-status-dialog.tsx +++ b/components/vendor-regular-registrations/document-status-dialog.tsx @@ -387,25 +387,6 @@ export function DocumentStatusDialog({ </div> </div> - {/* 안전적격성 평가 */} - <div> - <h3 className="text-lg font-semibold mb-4">안전적격성 평가</h3> - <div className="p-4 border rounded-lg"> - <div className="flex items-center justify-between"> - <div className="flex items-center gap-2"> - <StatusIcon status={!!registration.safetyQualificationContent} /> - <span>안전적격성 평가</span> - </div> - <StatusBadge status={!!registration.safetyQualificationContent} /> - </div> - {registration.safetyQualificationContent && ( - <div className="mt-3 p-3 bg-gray-50 rounded"> - <p className="text-sm">{registration.safetyQualificationContent}</p> - </div> - )} - </div> - </div> - {/* 추가 정보 */} <div> <h3 className="text-lg font-semibold mb-4">추가 정보</h3> |
