summaryrefslogtreecommitdiff
path: root/lib/vendor-investigation/table/investigation-result-sheet.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-investigation/table/investigation-result-sheet.tsx')
-rw-r--r--lib/vendor-investigation/table/investigation-result-sheet.tsx580
1 files changed, 265 insertions, 315 deletions
diff --git a/lib/vendor-investigation/table/investigation-result-sheet.tsx b/lib/vendor-investigation/table/investigation-result-sheet.tsx
index b7577daa..36000333 100644
--- a/lib/vendor-investigation/table/investigation-result-sheet.tsx
+++ b/lib/vendor-investigation/table/investigation-result-sheet.tsx
@@ -3,7 +3,7 @@
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
-import { CalendarIcon, Loader, X, Download } from "lucide-react"
+import { CalendarIcon, Loader, X, Download, AlertTriangle } from "lucide-react"
import { format } from "date-fns"
import { toast } from "sonner"
import { updateVendorInvestigationResultAction } from "../service"
@@ -68,6 +68,7 @@ import { updateVendorInvestigationAction, getInvestigationAttachments, deleteInv
import { VendorInvestigationsViewWithContacts } from "@/config/vendorInvestigationsColumnsConfig"
import prettyBytes from "pretty-bytes"
import { downloadFile } from "@/lib/file-download"
+import { Dialog as SystemDialog, DialogContent as SystemDialogContent, DialogHeader as SystemDialogHeader, DialogTitle as SystemDialogTitle, DialogFooter as SystemDialogFooter } from "@/components/ui/dialog"
interface InvestigationResultSheetProps extends React.ComponentPropsWithoutRef<typeof Sheet> {
investigation: VendorInvestigationsViewWithContacts | null
@@ -117,6 +118,11 @@ export function InvestigationResultSheet({
const [loadingAttachments, setLoadingAttachments] = React.useState(false)
const [uploadingFiles, setUploadingFiles] = React.useState(false)
+ // 불합격 안내 팝업 상태
+ const [showRejectedDialog, setShowRejectedDialog] = React.useState(false)
+ // 보완 세부 항목 (재실사/자료제출)
+ const [supplementType, setSupplementType] = React.useState<string>("")
+
// RHF + Zod
const form = useForm<UpdateVendorInvestigationResultSchema>({
resolver: zodResolver(updateVendorInvestigationResultSchema),
@@ -130,6 +136,39 @@ export function InvestigationResultSheet({
},
})
+ // 평가점수 변화 → 자동 평가 & 보완 타입 초기화
+ React.useEffect(() => {
+ const score = form.watch("evaluationScore")
+ let nextResult: string | undefined = undefined
+ if (typeof score === "number") {
+ if (score >= 80) nextResult = "APPROVED"
+ else if (score >= 70) {
+ // 70~79점일 때는 보완방법 선택을 기다리므로 바로 설정하지 않음
+ nextResult = undefined
+ setSupplementType("")
+ }
+ else if (score < 70) nextResult = "REJECTED"
+ }
+ if (nextResult) {
+ form.setValue("evaluationResult", nextResult as any)
+ } else if (score >= 70 && score < 80) {
+ // 70~79점 범위에서는 보완방법 선택이 필요하다는 표시
+ form.setValue("evaluationResult", "SUPPLEMENT" as any)
+ }
+ }, [form.watch("evaluationScore")])
+
+ // 보완방법 선택 변화 → 평가결과 변경
+ React.useEffect(() => {
+ // 70~79점 범위에서만 보완방법 선택에 따라 결과 변경
+ const score = form.watch("evaluationScore")
+ if (typeof score === "number" && score >= 70 && score < 80) {
+ if (supplementType === "REINSPECT")
+ form.setValue("evaluationResult", "SUPPLEMENT_REINSPECT" as any)
+ else if (supplementType === "DOCUMENT")
+ form.setValue("evaluationResult", "SUPPLEMENT_DOCUMENT" as any)
+ }
+ }, [supplementType, form.watch("evaluationScore")])
+
// investigation이 변경될 때마다 폼 리셋
React.useEffect(() => {
if (investigation) {
@@ -214,9 +253,9 @@ export function InvestigationResultSheet({
// 파일 업로드 섹션 렌더링
const renderFileUploadSection = () => {
- const currentStatus = form.watch("investigationStatus")
+ const currentStatus = form.watch("evaluationResult") as string | undefined
const selectedFiles = form.watch("attachments") as File[] | undefined
- const config = getFileUploadConfig(currentStatus)
+ const config = getFileUploadConfig(currentStatus ?? "")
if (!config.enabled) return null
@@ -454,6 +493,26 @@ export function InvestigationResultSheet({
return
}
+ // 보완-서류제출 선택 시 메일 발송
+ if (values.evaluationResult === "SUPPLEMENT_DOCUMENT") {
+ try {
+ const { requestInvestigationSupplementAction } = await import('../service')
+ const mailResult = await requestInvestigationSupplementAction({
+ investigationId: values.investigationId,
+ vendorId: investigation?.vendorId || 0,
+ comment: values.investigationNotes || "실사 보완이 필요합니다. 첨부된 내용을 확인하시고 필요한 자료를 제출해 주시기 바랍니다."
+ })
+
+ if (!mailResult.success) {
+ console.warn("보완 메일 발송 실패:", mailResult.error)
+ toast.warning("실사 결과는 저장되었지만 보완 메일 발송에 실패했습니다.")
+ }
+ } catch (mailError) {
+ console.warn("보완 메일 발송 중 오류:", mailError)
+ toast.warning("실사 결과는 저장되었지만 보완 메일 발송에 실패했습니다.")
+ }
+ }
+
// 2) 파일이 있으면 업로드
if (values.attachments && values.attachments.length > 0) {
setUploadingFiles(true)
@@ -484,325 +543,216 @@ export function InvestigationResultSheet({
})
}
- // 디버깅을 위한 버튼 클릭 핸들러
+ // 저장버튼 커스텀(불합격시: 팝업 → 확인하면 제출 / 아니면 중단)
const handleSaveClick = async () => {
- console.log("저장 버튼 클릭됨")
- console.log("현재 폼 값:", form.getValues())
- console.log("폼 에러:", form.formState.errors)
-
- // 폼 검증 실행
- const isValid = await form.trigger()
- console.log("폼 검증 결과:", isValid)
-
- if (isValid) {
- form.handleSubmit(onSubmit)()
- } else {
- console.log("폼 검증 실패, 에러:", form.formState.errors)
+ const score = form.getValues("evaluationScore")
+ const result = form.getValues("evaluationResult")
+ if (result === "REJECTED" && !showRejectedDialog) {
+ setShowRejectedDialog(true)
+ return
}
+ const isValid = await form.trigger()
+ if (isValid) form.handleSubmit(onSubmit)()
+ }
+ // 불합격 안내(확정) 처리
+ const handleRejectedConfirm = () => {
+ setShowRejectedDialog(false)
+ form.handleSubmit(onSubmit)()
}
return (
- <Sheet {...props}>
- <SheetContent className="flex flex-col h-full sm:max-w-xl" >
- <SheetHeader className="text-left flex-shrink-0">
- <SheetTitle>실사 결과 입력</SheetTitle>
- <SheetDescription>
- {investigation?.vendorName && (
- <span className="font-medium">{investigation.vendorName}</span>
- )}의 실사 결과를 입력합니다.
- </SheetDescription>
- </SheetHeader>
-
- <div className="flex-1 overflow-y-auto py-4">
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit(onSubmit)}
- className="flex flex-col gap-4"
- id="update-investigation-form"
- >
- {/* 실사 상태 - 주석처리 (실사 결과 입력에서는 자동으로 완료됨/취소됨/보완요구됨으로 변경) */}
- {/* <FormField
- control={form.control}
- name="investigationStatus"
- render={({ field }) => (
- <FormItem>
- <FormLabel>실사 상태</FormLabel>
- <FormControl>
- <Select value={field.value} onValueChange={field.onChange}>
- <SelectTrigger>
- <SelectValue placeholder="상태를 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- <SelectGroup>
- <SelectItem value="PLANNED">계획됨</SelectItem>
- <SelectItem value="IN_PROGRESS">진행 중</SelectItem>
- <SelectItem value="COMPLETED">완료됨</SelectItem>
- <SelectItem value="CANCELED">취소됨</SelectItem>
- <SelectItem value="SUPPLEMENT_REQUIRED">보완 요구됨</SelectItem>
- <SelectItem value="RESULT_SENT">실사결과발송</SelectItem>
- </SelectGroup>
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- /> */}
-
- {/* 실사 주소 - 주석처리 (실사 진행 관리에서 처리) */}
- {/* <FormField
- control={form.control}
- name="investigationAddress"
- render={({ field }) => (
- <FormItem>
- <FormLabel>실사 주소</FormLabel>
- <FormControl>
- <Textarea
- placeholder="실사가 진행될 주소를 입력하세요..."
- {...field}
- className="min-h-[60px]"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- /> */}
-
- {/* 실사 방법 - 주석처리 (실사 진행 관리에서 처리) */}
- {/* <FormField
- control={form.control}
- name="investigationMethod"
- render={({ field }) => (
- <FormItem>
- <FormLabel>실사 방법</FormLabel>
- <FormControl>
- <Select value={field.value || ""} onValueChange={field.onChange}>
- <SelectTrigger>
- <SelectValue placeholder="실사 방법을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- <SelectGroup>
- <SelectItem value="PURCHASE_SELF_EVAL">구매자체평가</SelectItem>
- <SelectItem value="DOCUMENT_EVAL">서류평가</SelectItem>
- <SelectItem value="PRODUCT_INSPECTION">제품검사평가</SelectItem>
- <SelectItem value="SITE_VISIT_EVAL">방문실사평가</SelectItem>
- </SelectGroup>
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- /> */}
-
- {/* 실사 수행 예정일 - 주석처리 (실사 진행 관리에서 처리) */}
- {/* <FormField
- control={form.control}
- name="forecastedAt"
- render={({ field }) => (
- <FormItem className="flex flex-col">
- <FormLabel>실사 수행 예정일</FormLabel>
- <Popover>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- className={`w-full pl-3 text-left font-normal ${!field.value && "text-muted-foreground"}`}
- >
- {field.value ? (
- format(field.value, "yyyy년 MM월 dd일")
- ) : (
- <span>날짜를 선택하세요</span>
- )}
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-auto p-0" align="start">
- <Calendar
- mode="single"
- selected={field.value}
- onSelect={field.onChange}
- initialFocus
- />
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )}
- /> */}
-
- {/* 실사 확정일 - 주석처리 (실사 진행 관리에서 처리) */}
- {/* <FormField
- control={form.control}
- name="confirmedAt"
- render={({ field }) => (
- <FormItem className="flex flex-col">
- <FormLabel>실사 계획 확정일</FormLabel>
- <Popover>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- className={`w-full pl-3 text-left font-normal ${!field.value && "text-muted-foreground"}`}
- >
- {field.value ? (
- format(field.value, "yyyy년 MM월 dd일")
- ) : (
- <span>날짜를 선택하세요</span>
- )}
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-auto p-0" align="start">
- <Calendar
- mode="single"
- selected={field.value}
- onSelect={field.onChange}
- initialFocus
+ <>
+ <Sheet {...props}>
+ <SheetContent className="flex flex-col h-full sm:max-w-xl" >
+ <SheetHeader className="text-left flex-shrink-0">
+ <SheetTitle>실사 결과 입력</SheetTitle>
+ <SheetDescription>
+ {investigation?.vendorName && (
+ <span className="font-medium">{investigation.vendorName}</span>
+ )}의 실사 결과를 입력합니다.
+ </SheetDescription>
+ </SheetHeader>
+
+ <div className="flex-1 overflow-y-auto py-4">
+ <Form {...form}>
+ <form
+ onSubmit={form.handleSubmit(onSubmit)}
+ className="flex flex-col gap-4"
+ id="update-investigation-form"
+ >
+ {/* 실제 실사일 */}
+ <FormField
+ control={form.control}
+ name="completedAt"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>실제 실사일<span className="text-red-500 ml-1">*</span></FormLabel>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ className={`w-full pl-3 text-left font-normal ${!field.value && "text-muted-foreground"}`}
+ >
+ {field.value ? (
+ format(field.value, "yyyy년 MM월 dd일")
+ ) : (
+ <span>날짜를 선택하세요</span>
+ )}
+ <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+ </Button>
+ </FormControl>
+ </PopoverTrigger>
+ <PopoverContent className="w-auto p-0" align="start">
+ <Calendar mode="single" selected={field.value} onSelect={field.onChange} initialFocus />
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 평가 점수 */}
+ <FormField
+ control={form.control}
+ name="evaluationScore"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>평가 점수<span className="text-red-500 ml-1">*</span></FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ min={0}
+ max={100}
+ placeholder="0-100점"
+ maxLength={3}
+ {...field}
+ value={field.value || ""}
+ onChange={e => {
+ const inputValue = e.target.value
+
+ // 빈 값이거나 숫자가 아닌 경우
+ if (inputValue === "") {
+ field.onChange(undefined)
+ return
+ }
+
+ // 3자리 초과 입력 방지
+ if (inputValue.length > 3) {
+ return
+ }
+
+ const numericValue = parseInt(inputValue, 10)
+
+ // 100 이상 입력 시 alert
+ if (numericValue > 100) {
+ toast.error("평가 점수는 100점을 초과할 수 없습니다.")
+ return
+ }
+
+ field.onChange(numericValue)
+ }}
/>
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )}
- /> */}
-
- {/* 실제 실사일 */}
- <FormField
- control={form.control}
- name="completedAt"
- render={({ field }) => (
- <FormItem className="flex flex-col">
- <FormLabel>실제 실사일</FormLabel>
- <Popover>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- className={`w-full pl-3 text-left font-normal ${!field.value && "text-muted-foreground"}`}
- >
- {field.value ? (
- format(field.value, "yyyy년 MM월 dd일")
- ) : (
- <span>날짜를 선택하세요</span>
- )}
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-auto p-0" align="start">
- <Calendar
- mode="single"
- selected={field.value}
- onSelect={field.onChange}
- initialFocus
- />
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 평가 점수 */}
- <FormField
- control={form.control}
- name="evaluationScore"
- render={({ field }) => (
- <FormItem>
- <FormLabel>평가 점수</FormLabel>
- <FormControl>
- <Input
- type="number"
- min={0}
- max={100}
- placeholder="0-100점"
- {...field}
- value={field.value || ""}
- onChange={(e) => {
- const value = e.target.value === "" ? undefined : parseInt(e.target.value, 10)
- field.onChange(value)
- }}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 평가 결과 */}
- <FormField
- control={form.control}
- name="evaluationResult"
- render={({ field }) => (
- <FormItem>
- <FormLabel>평가 결과</FormLabel>
- <FormControl>
- <Select value={field.value || ""} onValueChange={field.onChange}>
- <SelectTrigger>
- <SelectValue placeholder="평가 결과를 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- <SelectGroup>
- <SelectItem value="APPROVED">승인</SelectItem>
- <SelectItem value="SUPPLEMENT">보완</SelectItem>
- <SelectItem value="SUPPLEMENT_REINSPECT">보완-재실사</SelectItem>
- <SelectItem value="SUPPLEMENT_DOCUMENT">보완-서류제출</SelectItem>
- <SelectItem value="REJECTED">불가</SelectItem>
- </SelectGroup>
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* QM 의견 */}
- <FormField
- control={form.control}
- name="investigationNotes"
- render={({ field }) => (
- <FormItem>
- <FormLabel>QM 의견</FormLabel>
- <FormControl>
- <Textarea
- placeholder="실사에 대한 QM 의견을 입력하세요..."
- {...field}
- className="min-h-[80px]"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 평가 결과 VIEW (자동) */}
+ <div>
+ <FormLabel>평가 결과</FormLabel>
+ <div className="min-h-10 flex items-center gap-2 mt-1 font-bold">
+ {(() => {
+ const result = form.watch("evaluationResult")
+ 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 === "REJECTED") return <span className="text-destructive">불합격</span>
+ return <span className="text-muted-foreground">-</span>
+ })()}
+ </div>
+ </div>
+
+ {/* 보완 세부항목(70~79점) */}
+ {(() => {
+ const score = form.watch("evaluationScore")
+ return typeof score === "number" && score >= 70 && score < 80
+ })() && (
+ <div>
+ <FormLabel>보완 방법<span className="text-red-500 ml-1">*</span></FormLabel>
+ <Select value={supplementType} onValueChange={setSupplementType}>
+ <SelectTrigger>
+ <SelectValue placeholder="선택" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectGroup>
+ <SelectItem value="REINSPECT">재실사</SelectItem>
+ <SelectItem value="DOCUMENT">자료제출</SelectItem>
+ </SelectGroup>
+ </SelectContent>
+ </Select>
+ </div>
)}
- />
-
- {/* 파일 첨부 섹션 */}
- {renderFileUploadSection()}
- </form>
- </Form>
- </div>
-
- {/* Footer Buttons */}
- <SheetFooter className="gap-2 pt-2 sm:space-x-0 flex-shrink-0">
- <SheetClose asChild>
- <Button type="button" variant="outline" disabled={isPending || uploadingFiles}>
- 취소
+
+ {/* QM 의견 */}
+ <FormField
+ control={form.control}
+ name="investigationNotes"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>QM 의견</FormLabel>
+ <FormControl>
+ <Textarea placeholder="실사에 대한 QM 의견을 입력하세요..." {...field} className="min-h-[80px]" />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 파일 첨부 섹션 */}
+ {renderFileUploadSection()}
+ </form>
+ </Form>
+ </div>
+
+ {/* Footer Buttons */}
+ <SheetFooter className="gap-2 pt-2 sm:space-x-0 flex-shrink-0">
+ <SheetClose asChild>
+ <Button type="button" variant="outline" disabled={isPending || uploadingFiles}>
+ 취소
+ </Button>
+ </SheetClose>
+ <Button
+ disabled={isPending || uploadingFiles}
+ onClick={handleSaveClick}
+ >
+ {(isPending || uploadingFiles) && (
+ <Loader className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />
+ )}
+ {uploadingFiles ? "업로드 중..." : isPending ? "저장 중..." : "저장"}
</Button>
- </SheetClose>
- <Button
- disabled={isPending || uploadingFiles}
- onClick={handleSaveClick}
- >
- {(isPending || uploadingFiles) && (
- <Loader className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />
- )}
- {uploadingFiles ? "업로드 중..." : isPending ? "저장 중..." : "저장"}
- </Button>
- </SheetFooter>
- </SheetContent>
- </Sheet>
+ </SheetFooter>
+ </SheetContent>
+ </Sheet>
+
+ {/* 불합격 안내 팝업 */}
+ <SystemDialog open={showRejectedDialog} onOpenChange={setShowRejectedDialog}>
+ <SystemDialogContent>
+ <SystemDialogHeader>
+ <SystemDialogTitle><AlertTriangle className="mr-2 inline h-6 w-6 text-destructive" />불합격 확정 시 안내</SystemDialogTitle>
+ </SystemDialogHeader>
+ <div className="mt-2 mb-4 text-base leading-relaxed">
+ 불합격 확정 시 <b>결과입력완료일부터 1년간 동일 건에 대한 실사는 불가합니다.</b><br/>
+ 정말 확정 처리하시겠습니까?
+ </div>
+ <SystemDialogFooter className="flex flex-row justify-end gap-2">
+ <Button variant="outline" onClick={() => setShowRejectedDialog(false)}>취소</Button>
+ <Button variant="destructive" onClick={handleRejectedConfirm}>확인</Button>
+ </SystemDialogFooter>
+ </SystemDialogContent>
+ </SystemDialog>
+ </>
)
} \ No newline at end of file