diff options
Diffstat (limited to 'lib/evaluation/table/periodic-evaluation-action-dialogs.tsx')
| -rw-r--r-- | lib/evaluation/table/periodic-evaluation-action-dialogs.tsx | 373 |
1 files changed, 373 insertions, 0 deletions
diff --git a/lib/evaluation/table/periodic-evaluation-action-dialogs.tsx b/lib/evaluation/table/periodic-evaluation-action-dialogs.tsx new file mode 100644 index 00000000..30ff9535 --- /dev/null +++ b/lib/evaluation/table/periodic-evaluation-action-dialogs.tsx @@ -0,0 +1,373 @@ +"use client" + +import * as React from "react" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { FileText, Users, Calendar, Send } from "lucide-react" +import { toast } from "sonner" +import { PeriodicEvaluationView } from "@/db/schema" +import { checkExistingSubmissions, requestDocumentsFromVendors } from "../service" + + +// ================================================================ +// 2. 협력업체 자료 요청 다이얼로그 +// ================================================================ +interface RequestDocumentsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + evaluations: PeriodicEvaluationView[] + onSuccess: () => void +} + +interface EvaluationWithSubmissionStatus extends PeriodicEvaluationView { + hasExistingSubmission?: boolean + submissionDate?: Date | null +} + +export function RequestDocumentsDialog({ + open, + onOpenChange, + evaluations, + onSuccess, +}: RequestDocumentsDialogProps) { + + console.log(evaluations) + + const [isLoading, setIsLoading] = React.useState(false) + const [isCheckingStatus, setIsCheckingStatus] = React.useState(false) + const [message, setMessage] = React.useState("") + const [evaluationsWithStatus, setEvaluationsWithStatus] = React.useState<EvaluationWithSubmissionStatus[]>([]) + + // 제출대기 상태인 평가들만 필터링 + const pendingEvaluations = React.useMemo(() => + evaluations.filter(e => e.status === "PENDING_SUBMISSION"), + [evaluations] + ) + + React.useEffect(() => { + if (!open) return; + + // 대기 중 평가가 없으면 초기화 + if (pendingEvaluations.length === 0) { + setEvaluationsWithStatus([]); + setIsCheckingStatus(false); + return; + } + + // 상태 확인 + (async () => { + setIsCheckingStatus(true); + try { + const ids = pendingEvaluations.map(e => e.id); + const existing = await checkExistingSubmissions(ids); + + setEvaluationsWithStatus( + pendingEvaluations.map(e => ({ + ...e, + hasExistingSubmission: existing.some(s => s.periodicEvaluationId === e.id), + submissionDate: existing.find(s => s.periodicEvaluationId === e.id)?.createdAt ?? null, + })), + ); + } catch (err) { + console.error(err); + setEvaluationsWithStatus( + pendingEvaluations.map(e => ({ ...e, hasExistingSubmission: false })), + ); + } finally { + setIsCheckingStatus(false); + } + })(); + }, [open, pendingEvaluations]); // 함수 대신 값에만 의존 + + // 새 요청과 재요청 분리 + const newRequests = evaluationsWithStatus.filter(e => !e.hasExistingSubmission) + const reRequests = evaluationsWithStatus.filter(e => e.hasExistingSubmission) + + const handleSubmit = async () => { + if (!message.trim()) { + toast.error("요청 메시지를 입력해주세요.") + return + } + + setIsLoading(true) + try { + // 서버 액션 데이터 준비 + const requestData = evaluationsWithStatus.map(evaluation => ({ + periodicEvaluationId: evaluation.id, + companyId: evaluation.vendorId, + evaluationYear: evaluation.evaluationYear, + evaluationRound: evaluation.evaluationPeriod, + message: message.trim() + })) + + // 서버 액션 호출 + const result = await requestDocumentsFromVendors(requestData) + + if (result.success) { + toast.success(result.message) + onSuccess() + onOpenChange(false) + setMessage("") + } else { + toast.error(result.message) + } + } catch (error) { + console.error('Error requesting documents:', error) + toast.error("자료 요청 발송 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-2xl"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <FileText className="size-4" /> + 협력업체 자료 요청 + </DialogTitle> + <DialogDescription> + 선택된 평가의 협력업체들에게 평가 자료 제출을 요청합니다. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {isCheckingStatus ? ( + <div className="flex items-center justify-center py-8"> + <div className="text-sm text-muted-foreground">요청 상태를 확인하고 있습니다...</div> + </div> + ) : ( + <> + {/* 신규 요청 대상 업체 */} + {newRequests.length > 0 && ( + <Card> + <CardHeader className="pb-3"> + <CardTitle className="text-sm text-blue-600"> + 신규 요청 대상 ({newRequests.length}개 업체) + </CardTitle> + </CardHeader> + <CardContent className="space-y-2 max-h-32 overflow-y-auto"> + {newRequests.map((evaluation) => ( + <div + key={evaluation.id} + className="flex items-center justify-between text-sm p-2 bg-blue-50 rounded" + > + <span className="font-medium">{evaluation.vendorName}</span> + <div className="flex gap-2"> + <Badge variant="outline">{evaluation.vendorCode}</Badge> + <Badge variant="default" className="bg-blue-600">신규</Badge> + </div> + </div> + ))} + </CardContent> + </Card> + )} + + {/* 재요청 대상 업체 */} + {reRequests.length > 0 && ( + <Card> + <CardHeader className="pb-3"> + <CardTitle className="text-sm text-orange-600"> + 재요청 대상 ({reRequests.length}개 업체) + </CardTitle> + </CardHeader> + <CardContent className="space-y-2 max-h-32 overflow-y-auto"> + {reRequests.map((evaluation) => ( + <div + key={evaluation.id} + className="flex items-center justify-between text-sm p-2 bg-orange-50 rounded" + > + <span className="font-medium">{evaluation.vendorName}</span> + <div className="flex gap-2"> + <Badge variant="outline">{evaluation.vendorCode}</Badge> + <Badge variant="secondary" className="bg-orange-100"> + 재요청 + </Badge> + {evaluation.submissionDate && ( + <span className="text-xs text-muted-foreground"> + {new Intl.DateTimeFormat("ko-KR", { + month: "2-digit", + day: "2-digit", + }).format(new Date(evaluation.submissionDate))} + </span> + )} + </div> + </div> + ))} + </CardContent> + </Card> + )} + + {/* 요청 대상이 없는 경우 */} + {!isCheckingStatus && evaluationsWithStatus.length === 0 && ( + <Card> + <CardContent className="pt-6"> + <div className="text-center text-sm text-muted-foreground"> + 요청할 수 있는 평가가 없습니다. + </div> + </CardContent> + </Card> + )} + </> + )} + + {/* 요청 메시지 */} + <div className="space-y-2"> + <Label htmlFor="message">요청 메시지</Label> + <Textarea + id="message" + placeholder="협력업체에게 전달할 메시지를 입력하세요..." + value={message} + onChange={(e) => setMessage(e.target.value)} + rows={4} + disabled={isCheckingStatus} + /> + </div> + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isLoading || isCheckingStatus} + > + 취소 + </Button> + <Button + onClick={handleSubmit} + disabled={isLoading || isCheckingStatus || evaluationsWithStatus.length === 0} + > + <Send className="size-4 mr-2" /> + {isLoading ? "발송 중..." : + `요청 발송 (신규 ${newRequests.length}개${reRequests.length > 0 ? `, 재요청 ${reRequests.length}개` : ''})`} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +} + + + +// ================================================================ +// 3. 평가자 평가 요청 다이얼로그 +// ================================================================ +interface RequestEvaluationDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + evaluations: PeriodicEvaluationView[] + onSuccess: () => void +} + +export function RequestEvaluationDialog({ + open, + onOpenChange, + evaluations, + onSuccess, +}: RequestEvaluationDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [message, setMessage] = React.useState("") + + // 제출완료 상태인 평가들만 필터링 + const submittedEvaluations = evaluations.filter(e => e.status === "SUBMITTED") + + const handleSubmit = async () => { + if (!message.trim()) { + toast.error("요청 메시지를 입력해주세요.") + return + } + + setIsLoading(true) + try { + // TODO: 평가자들에게 평가 요청 API 호출 + toast.success(`${submittedEvaluations.length}개 평가에 대한 평가 요청이 발송되었습니다.`) + onSuccess() + onOpenChange(false) + setMessage("") + } catch (error) { + console.error('Error requesting evaluation:', error) + toast.error("평가 요청 발송 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-2xl"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Users className="size-4" /> + 평가자 평가 요청 + </DialogTitle> + <DialogDescription> + 선택된 평가들에 대해 평가자들에게 평가를 요청합니다. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {/* 대상 평가 목록 */} + <Card> + <CardHeader className="pb-3"> + <CardTitle className="text-sm"> + 평가 대상 ({submittedEvaluations.length}개 평가) + </CardTitle> + </CardHeader> + <CardContent className="space-y-2 max-h-32 overflow-y-auto"> + {submittedEvaluations.map((evaluation) => ( + <div + key={evaluation.id} + className="flex items-center justify-between text-sm" + > + <span className="font-medium">{evaluation.vendorName}</span> + <div className="flex gap-2"> + <Badge variant="outline">{evaluation.evaluationPeriod}</Badge> + <Badge variant="secondary">제출완료</Badge> + </div> + </div> + ))} + </CardContent> + </Card> + + {/* 요청 메시지 */} + <div className="space-y-2"> + <Label htmlFor="evaluation-message">요청 메시지</Label> + <Textarea + id="evaluation-message" + placeholder="평가자들에게 전달할 메시지를 입력하세요..." + value={message} + onChange={(e) => setMessage(e.target.value)} + rows={4} + /> + </div> + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button onClick={handleSubmit} disabled={isLoading}> + <Send className="size-4 mr-2" /> + {isLoading ? "발송 중..." : `${submittedEvaluations.length}개 평가 요청`} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
