diff options
Diffstat (limited to 'components/pq/pq-review-detail.tsx')
| -rw-r--r-- | components/pq/pq-review-detail.tsx | 888 |
1 files changed, 0 insertions, 888 deletions
diff --git a/components/pq/pq-review-detail.tsx b/components/pq/pq-review-detail.tsx deleted file mode 100644 index 4f897a2b..00000000 --- a/components/pq/pq-review-detail.tsx +++ /dev/null @@ -1,888 +0,0 @@ -"use client" - -import React from "react" -import { Button } from "@/components/ui/button" -import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" -import { Textarea } from "@/components/ui/textarea" -import { useToast } from "@/hooks/use-toast" -import { - PQGroupData, - requestPqChangesAction, - updateVendorStatusAction, - updateProjectPQStatusAction, - getItemReviewLogsAction -} from "@/lib/pq/service" -import { Vendor } from "@/db/schema/vendors" -import { Separator } from "@/components/ui/separator" -import { Badge } from "@/components/ui/badge" -import { ChevronsUpDown, MessagesSquare, Download, Loader2, X } from "lucide-react" -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible" -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" -import { Card } from "@/components/ui/card" -import { formatDate } from "@/lib/utils" -import { downloadFileAction } from "@/lib/downloadFile" -import { useSession } from "next-auth/react" // Importando o hook do next-auth - -// 코멘트 상태를 위한 인터페이스 정의 -interface PendingComment { - answerId: number; - checkPoint: string; - code: string; - comment: string; - createdAt: Date; -} - -interface ReviewLog { - id: number - reviewerComment: string - reviewerName: string | null - createdAt: Date -} - -// Updated props interface to support both general and project PQs -interface VendorPQAdminReviewProps { - data: PQGroupData[] - vendor: Vendor - projectId?: number - projectName?: string - projectStatus?: string - // loadData: () => Promise<PQGroupData[]> - loadData: (vendorId: number, projectId?: number) => Promise<PQGroupData[]> - - pqType: 'general' | 'project' -} - -export default function VendorPQAdminReview({ - data, - vendor, - projectId, - projectName, - projectStatus, - loadData, - pqType -}: VendorPQAdminReviewProps) { - const { toast } = useToast() - const { data: session } = useSession() - const reviewerName = session?.user?.name || "Unknown Reviewer" - const reviewerId = session?.user?.id - - - // State for dynamically loaded data - const [pqData, setPqData] = React.useState<PQGroupData[]>(data) - const [isDataLoading, setIsDataLoading] = React.useState(false) - - // Load data if not provided initially (for tab switching) - React.useEffect(() => { - if (data.length === 0) { - const fetchData = async () => { - setIsDataLoading(true) - try { - const freshData = await loadData(vendor.id, projectId) - - setPqData(freshData) - } catch (error) { - console.error("Error loading PQ data:", error) - toast({ - title: "Error", - description: "Failed to load PQ data", - variant: "destructive" - }) - } finally { - setIsDataLoading(false) - } - } - fetchData() - } else { - setPqData(data) - } - }, [data, loadData, vendor.id, projectId, toast]) - - // 다이얼로그 상태들 - const [showRequestDialog, setShowRequestDialog] = React.useState(false) - const [showApproveDialog, setShowApproveDialog] = React.useState(false) - const [showRejectDialog, setShowRejectDialog] = React.useState(false) - - // 코멘트 상태들 - const [requestComment, setRequestComment] = React.useState("") - const [approveComment, setApproveComment] = React.useState("") - const [rejectComment, setRejectComment] = React.useState("") - const [isLoading, setIsLoading] = React.useState(false) - - // 항목별 코멘트 상태 추적 (메모리에만 저장) - const [pendingComments, setPendingComments] = React.useState<PendingComment[]>([]) - - // 코멘트 추가 핸들러 - 실제 서버 저장이 아닌 메모리에 저장 - const handleCommentAdded = (newComment: PendingComment) => { - setPendingComments(prev => [...prev, newComment]); - toast({ - title: "Comment Added", - description: `Comment added for ${newComment.code}. Please "Request Changes" to save.` - }); - } - - // 코멘트 삭제 핸들러 - const handleRemoveComment = (index: number) => { - setPendingComments(prev => prev.filter((_, i) => i !== index)); - } - - // 1) 승인 다이얼로그 표시 - const handleApprove = () => { - // 코멘트가 있는데 승인하려고 하면 경고 - if (pendingComments.length > 0) { - if (!confirm('You have unsaved comments. Are you sure you want to approve without requesting changes?')) { - return; - } - } - setShowApproveDialog(true) - } - - // 실제 승인 처리 - 일반 PQ와 프로젝트 PQ 분리 - const handleSubmitApprove = async () => { - try { - setIsLoading(true) - setShowApproveDialog(false) - - let res; - - if (pqType === 'general') { - // 일반 PQ 승인 - res = await updateVendorStatusAction(vendor.id, "PQ_APPROVED") - } else if (projectId) { - // 프로젝트 PQ 승인 - res = await updateProjectPQStatusAction({ - vendorId: vendor.id, - projectId, - status: "APPROVED", - comment: approveComment.trim() || undefined - }) - } - - if (res?.ok) { - toast({ - title: "Approved", - description: `${pqType === 'general' ? 'General' : 'Project'} PQ has been approved.` - }) - // 코멘트 초기화 - setPendingComments([]); - } else { - toast({ - title: "Error", - description: res?.error || "An error occurred", - variant: "destructive" - }) - } - } catch (error) { - toast({ title: "Error", description: String(error), variant: "destructive" }) - } finally { - setIsLoading(false) - setApproveComment("") - } - } - - // 2) 거부 다이얼로그 표시 - const handleReject = () => { - // 코멘트가 있는데 거부하려고 하면 경고 - if (pendingComments.length > 0) { - if (!confirm('You have unsaved comments. Are you sure you want to reject without requesting changes?')) { - return; - } - } - setShowRejectDialog(true) - } - - // 실제 거부 처리 - 일반 PQ와 프로젝트 PQ 분리 - const handleSubmitReject = async () => { - try { - setIsLoading(true) - setShowRejectDialog(false) - - if (!rejectComment.trim()) { - toast({ - title: "Error", - description: "Please provide a reason for rejection", - variant: "destructive" - }) - return; - } - - let res; - - if (pqType === 'general') { - // 일반 PQ 거부 - res = await updateVendorStatusAction(vendor.id, "REJECTED") - } else if (projectId) { - // 프로젝트 PQ 거부 - res = await updateProjectPQStatusAction({ - vendorId: vendor.id, - projectId, - status: "REJECTED", - comment: rejectComment - }) - } - - if (res?.ok) { - toast({ - title: "Rejected", - description: `${pqType === 'general' ? 'General' : 'Project'} PQ has been rejected.` - }) - // 코멘트 초기화 - setPendingComments([]); - } else { - toast({ - title: "Error", - description: res?.error || "An error occurred", - variant: "destructive" - }) - } - } catch (error) { - toast({ title: "Error", description: String(error), variant: "destructive" }) - } finally { - setIsLoading(false) - setRejectComment("") - } - } - - // 3) 변경 요청 다이얼로그 표시 - const handleRequestChanges = () => { - setShowRequestDialog(true) - } - - // 4) 변경 요청 처리 - 이제 프로젝트 ID 포함 - const handleSubmitRequestChanges = async () => { - try { - setIsLoading(true); - setShowRequestDialog(false); - - // 항목별 코멘트 준비 - answerId와 함께 checkPoint와 code도 전송 - const itemComments = pendingComments.map(pc => ({ - answerId: pc.answerId, - checkPoint: pc.checkPoint, - code: pc.code, - comment: pc.comment - })); - - // 서버 액션 호출 (프로젝트 ID 추가) - const res = await requestPqChangesAction({ - vendorId: vendor.id, - projectId: pqType === 'project' ? projectId : undefined, - comment: itemComments, - generalComment: requestComment || undefined, - reviewerName, - reviewerId - }); - - if (res.ok) { - toast({ - title: "Changes Requested", - description: `${pqType === 'general' ? 'Vendor' : 'Project'} was notified of your comments.`, - }); - // 코멘트 초기화 - setPendingComments([]); - } else { - toast({ - title: "Error", - description: res.error, - variant: "destructive" - }); - } - } catch (error) { - toast({ - title: "Error", - description: String(error), - variant: "destructive" - }); - } finally { - setIsLoading(false); - setRequestComment(""); - } - }; - - // 현재 상태에 따른 액션 버튼 비활성화 여부 판단 - const getDisabledState = () => { - if (pqType === 'general') { - // 일반 PQ는 vendor 상태에 따라 결정 - return vendor.status === 'PQ_APPROVED' || vendor.status === 'APPROVED'; - } else if (pqType === 'project' && projectStatus) { - // 프로젝트 PQ는 project 상태에 따라 결정 - return projectStatus === 'APPROVED' || projectStatus === 'REJECTED'; - } - return false; - }; - - const areActionsDisabled = getDisabledState(); - - return ( - <div className="space-y-4"> - {/* PQ Type indicators and status */} - {pqType === 'project' && projectName && ( - <div className="flex flex-col space-y-1 mb-4"> - <div className="flex items-center gap-2"> - <Badge variant="outline">{projectName}</Badge> - {projectStatus && ( - <Badge className={ - projectStatus === 'APPROVED' ? 'bg-green-100 text-green-800' : - projectStatus === 'REJECTED' ? 'bg-red-100 text-red-800' : - 'bg-blue-100 text-blue-800' - }> - {projectStatus} - </Badge> - )} - </div> - {areActionsDisabled && ( - <p className="text-sm text-muted-foreground"> - This PQ has already been { - pqType !== 'project' - ? (vendor.status === 'PQ_APPROVED' || vendor.status === 'APPROVED' ? 'approved' : 'rejected') - : (projectStatus === 'APPROVED' ? 'approved' : 'rejected') - }. No further actions can be taken. - </p> - )} - </div> - )} - - {/* Loading indicator */} - {isDataLoading && ( - <div className="flex justify-center items-center h-32"> - <Loader2 className="h-8 w-8 animate-spin text-primary" /> - </div> - )} - - {!isDataLoading && ( - <> - {/* Top header */} - <div className="flex items-center justify-between"> - <h2 className="text-2xl font-bold"> - {vendor.vendorCode} - {vendor.vendorName} {pqType === 'project' ? 'Project' : 'General'} PQ Review - </h2> - <div className="flex gap-2"> - <Button - variant="outline" - disabled={isLoading || areActionsDisabled} - onClick={handleReject} - > - Reject - </Button> - <Button - variant={pendingComments.length > 0 ? "default" : "outline"} - disabled={isLoading || areActionsDisabled} - onClick={handleRequestChanges} - > - Request Changes - {pendingComments.length > 0 && ( - <span className="ml-2 bg-white text-primary rounded-full h-5 min-w-5 inline-flex items-center justify-center text-xs px-1"> - {pendingComments.length} - </span> - )} - </Button> - <Button - disabled={isLoading || areActionsDisabled} - onClick={handleApprove} - > - Approve - </Button> - </div> - </div> - - <p className="text-sm text-muted-foreground"> - Review the submitted PQ items below, then approve, reject, or request more info. - </p> - - {/* 코멘트가 있을 때 알림 표시 */} - {pendingComments.length > 0 && ( - <div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md text-yellow-800"> - <p className="text-sm font-medium flex items-center"> - <span className="mr-2">⚠️</span> - You have {pendingComments.length} pending comments. Click "Request Changes" to save them. - </p> - </div> - )} - - <Separator /> - - {/* PQ 데이터 표시 */} - {pqData.length > 0 ? ( - <VendorPQReviewPageIntegrated - data={pqData} - onCommentAdded={handleCommentAdded} - /> - ) : ( - <div className="text-center py-10"> - <p className="text-muted-foreground">No PQ data available for review.</p> - </div> - )} - </> - )} - - {/* 변경 요청 다이얼로그 */} - <Dialog open={showRequestDialog} onOpenChange={setShowRequestDialog}> - <DialogContent className="max-w-3xl"> - <DialogHeader> - <DialogTitle>Request PQ Changes</DialogTitle> - <DialogDescription> - Review your comments and add any additional notes. The vendor will receive these changes. - </DialogDescription> - </DialogHeader> - - {/* 항목별 코멘트 목록 */} - {pendingComments.length > 0 && ( - <div className="border rounded-md p-2 space-y-2 max-h-[300px] overflow-y-auto"> - <h3 className="font-medium text-sm">Item Comments:</h3> - {pendingComments.map((comment, index) => ( - <div key={index} className="flex items-start gap-2 p-2 border rounded-md bg-muted/50"> - <div className="flex-1"> - <div className="flex items-center gap-2"> - <span className="text-sm font-medium">{comment.code}</span> - <span className="text-sm">{comment.checkPoint}</span> - </div> - <p className="text-sm mt-1">{comment.comment}</p> - <p className="text-xs text-muted-foreground mt-1"> - {formatDate(comment.createdAt, "KR")} - </p> - </div> - <Button - variant="ghost" - size="sm" - className="p-0 h-8 w-8" - onClick={() => handleRemoveComment(index)} - > - <X className="h-4 w-4" /> - </Button> - </div> - ))} - </div> - )} - - {/* 추가 코멘트 입력 */} - <div className="space-y-2 mt-2"> - <label className="text-sm font-medium"> - {pendingComments.length > 0 - ? "Additional comments (optional):" - : "Enter details about what should be modified:"} - </label> - <Textarea - value={requestComment} - onChange={(e) => setRequestComment(e.target.value)} - placeholder={pendingComments.length > 0 - ? "Add any additional notes..." - : "Please correct item #1, etc..."} - className="min-h-[100px]" - /> - </div> - - <DialogFooter> - <Button - variant="outline" - onClick={() => setShowRequestDialog(false)} - disabled={isLoading} - > - Cancel - </Button> - <Button - onClick={handleSubmitRequestChanges} - disabled={isLoading || (pendingComments.length === 0 && !requestComment.trim())} - > - Submit Changes - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - - {/* 승인 확인 다이얼로그 */} - <Dialog open={showApproveDialog} onOpenChange={setShowApproveDialog}> - <DialogContent> - <DialogHeader> - <DialogTitle>Confirm Approval</DialogTitle> - <DialogDescription> - Are you sure you want to approve this {pqType === 'project' ? 'project' : 'vendor'} PQ? You can add a comment if needed. - </DialogDescription> - </DialogHeader> - - <div className="space-y-2"> - <Textarea - value={approveComment} - onChange={(e) => setApproveComment(e.target.value)} - placeholder="Optional: Add any comments about this approval" - className="min-h-[100px]" - /> - </div> - - <DialogFooter> - <Button - variant="outline" - onClick={() => setShowApproveDialog(false)} - disabled={isLoading} - > - Cancel - </Button> - <Button - onClick={handleSubmitApprove} - disabled={isLoading} - > - Confirm Approval - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - - {/* 거부 확인 다이얼로그 */} - <Dialog open={showRejectDialog} onOpenChange={setShowRejectDialog}> - <DialogContent> - <DialogHeader> - <DialogTitle>Confirm Rejection</DialogTitle> - <DialogDescription> - Are you sure you want to reject this {pqType === 'project' ? 'project' : 'vendor'} PQ? Please provide a reason. - </DialogDescription> - </DialogHeader> - - <div className="space-y-2"> - <Textarea - value={rejectComment} - onChange={(e) => setRejectComment(e.target.value)} - placeholder="Required: Provide reason for rejection" - className="min-h-[150px]" - /> - </div> - - <DialogFooter> - <Button - variant="outline" - onClick={() => setShowRejectDialog(false)} - disabled={isLoading} - > - Cancel - </Button> - <Button - onClick={handleSubmitReject} - disabled={isLoading || !rejectComment.trim()} - variant="destructive" - > - Confirm Rejection - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - </div> - ) -} - -// 코멘트 추가 함수 인터페이스 -interface VendorPQReviewPageIntegratedProps { - data: PQGroupData[]; - onCommentAdded: (comment: PendingComment) => void; -} - -// 통합된 VendorPQReviewPage 컴포넌트 -function VendorPQReviewPageIntegrated({ data, onCommentAdded }: VendorPQReviewPageIntegratedProps) { - const { toast } = useToast(); - - // 파일 다운로드 함수 - 서버 액션 사용 - const handleFileDownload = async (filePath: string, fileName: string) => { - try { - toast({ - title: "Download Started", - description: `Preparing ${fileName} for download...`, - }); - - // 서버 액션 호출 - const result = await downloadFileAction(filePath); - - if (!result.ok || !result.data) { - throw new Error(result.error || 'Failed to download file'); - } - - // Base64 디코딩하여 Blob 생성 - const binaryString = atob(result.data.content); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - - // Blob 생성 및 다운로드 - const blob = new Blob([bytes.buffer], { type: result.data.mimeType }); - const url = URL.createObjectURL(blob); - - // 다운로드 링크 생성 및 클릭 - const a = document.createElement('a'); - a.href = url; - a.download = fileName; - document.body.appendChild(a); - a.click(); - - // 정리 - URL.revokeObjectURL(url); - document.body.removeChild(a); - - toast({ - title: "Download Complete", - description: `${fileName} downloaded successfully`, - }); - } catch (error) { - console.error('Download error:', error); - toast({ - title: "Download Error", - description: error instanceof Error ? error.message : "Failed to download file", - variant: "destructive" - }); - } - }; - - return ( - <div className="space-y-4"> - {data.map((group) => ( - <Collapsible key={group.groupName} defaultOpen> - <CollapsibleTrigger asChild> - <div className="flex items-center justify-between cursor-pointer p-3 bg-muted rounded"> - <h2 className="font-semibold text-lg">{group.groupName}</h2> - <Button variant="ghost" size="sm" className="p-0 h-7 w-7"> - <ChevronsUpDown className="h-4 w-4" /> - </Button> - </div> - </CollapsibleTrigger> - - <CollapsibleContent> - <Card className="mt-2 p-4"> - <Table> - <TableHeader> - <TableRow> - <TableHead className="w-[60px]">Code</TableHead> - <TableHead>Check Point</TableHead> - <TableHead>Answer</TableHead> - <TableHead className="w-[180px]">Attachments</TableHead> - <TableHead className="w-[60px] text-center">Comments</TableHead> - </TableRow> - </TableHeader> - - <TableBody> - {group.items.map((item) => ( - <TableRow key={item.criteriaId}> - <TableCell className="font-medium">{item.code}</TableCell> - <TableCell>{item.checkPoint}</TableCell> - - <TableCell> - {item.answer ? ( - <p className="whitespace-pre-wrap text-sm"> - {item.answer} - </p> - ) : ( - <p className="text-sm text-muted-foreground">(no answer)</p> - )} - </TableCell> - - <TableCell> - {item.attachments.length > 0 ? ( - <ul className="list-none space-y-1"> - {item.attachments.map((file) => ( - <li key={file.attachId} className="text-sm flex items-center"> - <button - className="text-blue-600 hover:text-blue-800 hover:underline flex items-center truncate max-w-[160px]" - onClick={() => handleFileDownload(file.filePath, file.fileName)} - > - <Download className="h-3 w-3 mr-1 flex-shrink-0" /> - <span className="truncate">{file.fileName}</span> - </button> - </li> - ))} - </ul> - ) : ( - <p className="text-sm text-muted-foreground">(none)</p> - )} - </TableCell> - - <TableCell className="text-center"> - <ItemCommentButton - item={item} - onCommentAdded={onCommentAdded} - /> - </TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </Card> - </CollapsibleContent> - </Collapsible> - ))} - </div> - ); -} - -// 항목 코멘트 버튼 컴포넌트 props -interface ItemCommentButtonProps { - item: any; // 항목 데이터 - onCommentAdded: (comment: PendingComment) => void; -} - -// 항목별 코멘트 버튼 컴포넌트 (기존 로그 표시 + 메모리에 새 코멘트 저장) -function ItemCommentButton({ item, onCommentAdded }: ItemCommentButtonProps) { - const { toast } = useToast(); - const [open, setOpen] = React.useState(false); - const [logs, setLogs] = React.useState<ReviewLog[]>([]); - const [newComment, setNewComment] = React.useState(""); - const [isLoading, setIsLoading] = React.useState(false); - const [hasComments, setHasComments] = React.useState(false); - - // If there's no answerId, item wasn't answered - if (!item.answerId) { - return <p className="text-xs text-muted-foreground">N/A</p>; - } - - // 기존 로그 가져오기 - const fetchLogs = React.useCallback(async () => { - try { - setIsLoading(true); - const res = await getItemReviewLogsAction({ answerId: item.answerId }); - - if (res.ok && res.data) { - setLogs(res.data); - // 코멘트 존재 여부 설정 - setHasComments(res.data.length > 0); - } else { - console.error("Error response:", res.error); - toast({ title: "Error", description: res.error, variant: "destructive" }); - } - } catch (error) { - console.error("Fetch error:", error); - toast({ title: "Error", description: String(error), variant: "destructive" }); - } finally { - setIsLoading(false); - } - }, [item.answerId, toast]); - - // 초기 로드 시 코멘트 존재 여부 확인 (아이콘 색상용) - React.useEffect(() => { - const checkComments = async () => { - try { - const res = await getItemReviewLogsAction({ answerId: item.answerId }); - if (res.ok && res.data) { - setHasComments(res.data.length > 0); - } - } catch (error) { - console.error("Error checking comments:", error); - } - }; - - checkComments(); - }, [item.answerId]); - - // open 상태가 변경될 때 로그 가져오기 - React.useEffect(() => { - if (open) { - fetchLogs(); - } - }, [open, fetchLogs]); - - // 다이얼로그 열기 - const handleButtonClick = React.useCallback(() => { - setOpen(true); - }, []); - - // 다이얼로그 상태 변경 - const handleOpenChange = React.useCallback((nextOpen: boolean) => { - setOpen(nextOpen); - }, []); - - // 코멘트 추가 처리 (메모리에만 저장) - const handleAddComment = React.useCallback(() => { - if (!newComment.trim()) return; - - setIsLoading(true); - - // 새 코멘트 생성 - const pendingComment: PendingComment = { - answerId: item.answerId, - checkPoint: item.checkPoint, - code: item.code, - comment: newComment.trim(), - createdAt: new Date() - }; - - // 부모 컴포넌트에 전달 - onCommentAdded(pendingComment); - - // 상태 초기화 - setNewComment(""); - setOpen(false); - setIsLoading(false); - }, [item, newComment, onCommentAdded]); - - return ( - <> - <Button variant="ghost" size="sm" onClick={handleButtonClick}> - <MessagesSquare - className={`h-4 w-4 ${hasComments ? 'text-blue-600' : ''}`} - /> - </Button> - - <Dialog open={open} onOpenChange={handleOpenChange}> - <DialogContent> - <DialogHeader> - <DialogTitle>{item.checkPoint}</DialogTitle> - <DialogDescription> - Review existing comments and add new ones - </DialogDescription> - </DialogHeader> - - {/* 기존 로그 섹션 */} - <div className="max-h-[200px] overflow-y-auto space-y-2"> - {isLoading ? ( - <div className="flex justify-center p-4"> - <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> - </div> - ) : logs.length > 0 ? ( - <div className="space-y-2"> - <h3 className="text-sm font-medium">Previous Comments:</h3> - {logs.map((log) => ( - <div key={log.id} className="p-2 border rounded text-sm"> - <p className="font-medium">{log.reviewerName}</p> - <p>{log.reviewerComment}</p> - <p className="text-xs text-muted-foreground"> - {formatDate(log.createdAt, "KR")} - </p> - </div> - ))} - </div> - ) : ( - <p className="text-sm text-muted-foreground">No previous comments yet.</p> - )} - </div> - - {/* 구분선 */} - {/* <Separator /> */} - - {/* 새 코멘트 추가 섹션 */} - <div className="space-y-2 mt-2"> - <div className="flex items-center justify-between"> - {/* <h3 className="text-sm font-medium">Add New Comment:</h3> */} - {/* <p className="text-xs text-muted-foreground"> - Comments will be saved when you click "Request Changes" - </p> */} - </div> - <Textarea - placeholder="Add your comment..." - value={newComment} - onChange={(e) => setNewComment(e.target.value)} - className="min-h-[100px]" - /> - <Button - onClick={handleAddComment} - disabled={isLoading || !newComment.trim()} - > - Add Comment - </Button> - </div> - </DialogContent> - </Dialog> - </> - ); -}
\ No newline at end of file |
