diff options
Diffstat (limited to 'lib/pcr/table/approve-reject-pcr-dialog.tsx')
| -rw-r--r-- | lib/pcr/table/approve-reject-pcr-dialog.tsx | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/lib/pcr/table/approve-reject-pcr-dialog.tsx b/lib/pcr/table/approve-reject-pcr-dialog.tsx new file mode 100644 index 00000000..065a30fa --- /dev/null +++ b/lib/pcr/table/approve-reject-pcr-dialog.tsx @@ -0,0 +1,231 @@ +"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+import { CheckCircle, XCircle, Loader2 } from "lucide-react"
+
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+} from "@/components/ui/dialog"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Textarea } from "@/components/ui/textarea"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import * as z from "zod"
+import { approvePcrAction, rejectPcrAction } from "@/lib/pcr/actions"
+import { PcrPoData } from "@/lib/pcr/types"
+
+// 승인 다이얼로그 스키마
+const approveSchema = z.object({
+ reason: z.string().optional(),
+})
+
+// 거절 다이얼로그 스키마
+const rejectSchema = z.object({
+ reason: z.string().min(1, "거절 사유를 입력해주세요."),
+})
+
+type ApproveFormValues = z.infer<typeof approveSchema>
+type RejectFormValues = z.infer<typeof rejectSchema>
+
+interface ApproveRejectPcrDialogProps {
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ pcrData?: PcrPoData | null
+ actionType: 'approve' | 'reject'
+ onSuccess?: () => void
+}
+
+export function ApproveRejectPcrDialog({
+ open,
+ onOpenChange,
+ pcrData,
+ actionType,
+ onSuccess,
+}: ApproveRejectPcrDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [internalOpen, setInternalOpen] = React.useState(false)
+
+ const dialogOpen = open !== undefined ? open : internalOpen
+ const setDialogOpen = onOpenChange || setInternalOpen
+
+ const isApprove = actionType === 'approve'
+
+ // 승인 폼
+ const approveForm = useForm<ApproveFormValues>({
+ resolver: zodResolver(approveSchema),
+ defaultValues: {
+ reason: "",
+ },
+ })
+
+ // 거절 폼
+ const rejectForm = useForm<RejectFormValues>({
+ resolver: zodResolver(rejectSchema),
+ defaultValues: {
+ reason: "",
+ },
+ })
+
+ const currentForm = isApprove ? approveForm : rejectForm
+
+ const handleSubmit = async (data: ApproveFormValues | RejectFormValues) => {
+ if (!pcrData) return
+
+ try {
+ setIsLoading(true)
+
+ const reason = 'reason' in data ? data.reason : undefined
+ const result = isApprove
+ ? await approvePcrAction(pcrData.id, reason)
+ : await rejectPcrAction(pcrData.id, reason || '')
+
+ if (result.success) {
+ toast.success(result.message)
+ currentForm.reset()
+ setDialogOpen(false)
+ onSuccess?.()
+ } else {
+ toast.error(result.error || `${isApprove ? '승인' : '거절'}에 실패했습니다.`)
+ }
+ } catch (error) {
+ console.error(`PCR ${isApprove ? '승인' : '거절'} 오류:`, error)
+ toast.error(`${isApprove ? '승인' : '거절'} 중 오류가 발생했습니다.`)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ if (!pcrData) return null
+
+ return (
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
+ <DialogContent className="max-w-md">
+ <DialogHeader>
+ <div className="flex items-center gap-3">
+ {isApprove ? (
+ <CheckCircle className="size-6 text-green-600" />
+ ) : (
+ <XCircle className="size-6 text-red-600" />
+ )}
+ <DialogTitle>
+ PCR {isApprove ? '승인' : '거절'} 확인
+ </DialogTitle>
+ </div>
+ <DialogDescription>
+ 다음 PCR을 {isApprove ? '승인' : '거절'}하시겠습니까?
+ </DialogDescription>
+ </DialogHeader>
+
+ {/* PCR 정보 표시 */}
+ <div className="space-y-3 p-4 bg-gray-50 rounded-lg">
+ <div className="grid grid-cols-2 gap-2 text-sm">
+ <div>
+ <span className="font-medium text-gray-600">PO/계약번호:</span>
+ <p className="font-mono text-gray-900">{pcrData.poContractNumber}</p>
+ </div>
+ <div>
+ <span className="font-medium text-gray-600">프로젝트:</span>
+ <p className="text-gray-900">{pcrData.project || '-'}</p>
+ </div>
+ <div>
+ <span className="font-medium text-gray-600">변경 구분:</span>
+ <p className="text-gray-900">{pcrData.changeType}</p>
+ </div>
+ <div>
+ <span className="font-medium text-gray-600">요청일자:</span>
+ <p className="text-gray-900">
+ {pcrData.pcrRequestDate.toLocaleDateString('ko-KR')}
+ </p>
+ </div>
+ </div>
+ {pcrData.details && (
+ <div>
+ <span className="font-medium text-gray-600 text-sm">상세:</span>
+ <p className="text-gray-900 text-sm mt-1">{pcrData.details}</p>
+ </div>
+ )}
+ </div>
+
+ {/* 승인/거절 사유 입력 폼 */}
+ <Form {...currentForm}>
+ <form onSubmit={currentForm.handleSubmit(handleSubmit)} className="space-y-4">
+ <FormField
+ control={currentForm.control}
+ name="reason"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>
+ {isApprove ? '승인 사유 (선택)' : '거절 사유 (필수)'}
+ </FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder={
+ isApprove
+ ? "승인 사유를 입력하세요 (선택사항)"
+ : "거절 사유를 입력하세요"
+ }
+ className="resize-none"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 버튼들 */}
+ <div className="flex justify-end gap-3 pt-4">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => setDialogOpen(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ variant={isApprove ? "default" : "destructive"}
+ disabled={isLoading}
+ >
+ {isLoading ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ {isApprove ? '승인 중...' : '거절 중...'}
+ </>
+ ) : (
+ <>
+ {isApprove ? (
+ <>
+ <CheckCircle className="mr-2 h-4 w-4" />
+ 승인
+ </>
+ ) : (
+ <>
+ <XCircle className="mr-2 h-4 w-4" />
+ 거절
+ </>
+ )}
+ </>
+ )}
+ </Button>
+ </div>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+}
|
