/* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; /* IMPORT */ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Checkbox } from '@/components/ui/checkbox'; import { ChevronsUpDown, X } from 'lucide-react'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dropzone, DropzoneDescription, DropzoneInput, DropzoneTitle, DropzoneUploadIcon, DropzoneZone, } from '@/components/ui/dropzone'; import { FileList, FileListAction, FileListDescription, FileListHeader, FileListIcon, FileListInfo, FileListItem, FileListName, FileListSize, } from '@/components/ui/file-list'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { format } from 'date-fns'; import { getProcurementManagerList, modifyRiskEvents } from '../service'; import { RISK_ADMIN_COMMENTS_LIST } from '@/config/risksConfig'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Textarea } from '@/components/ui/textarea'; import { toast } from 'sonner'; import { type RisksView, type User } from '@/db/schema'; import { useForm } from 'react-hook-form'; import { useEffect, useMemo, useState, useTransition } from 'react'; import UserComboBox from './user-combo-box'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { se } from 'date-fns/locale'; // ---------------------------------------------------------------------------------------------------- /* TYPES */ const risksMailFormSchema = z.object({ managerId: z.number({ required_error: '구매 담당자를 반드시 선택해야 해요.' }), adminComment: z.string().min(1, { message: '구매 담당자 의견을 반드시 작성해야 해요.' }), attachment: z .instanceof(File) .refine((file) => file.size <= 10485760, { message: '파일 크기는 10MB를 초과할 수 없어요.', }) .optional(), }); type RisksMailFormData = z.infer; interface RisksMailDialogProps { open: boolean, onOpenChange: (open: boolean) => void, riskDataList: RisksView[], onSuccess: () => void, }; // ---------------------------------------------------------------------------------------------------- /* CONSTATNS */ const ALWAYS_CHECKED_TYPES = ['종합등급', '신용등급', '현금흐름등급', 'WATCH등급']; // ---------------------------------------------------------------------------------------------------- /* RISKS MAIL DIALOG COPONENT */ function RisksMailDialog(props: RisksMailDialogProps) { const { open, onOpenChange, riskDataList, onSuccess } = props; const riskDataMap = useMemo(() => { return riskDataList.reduce((acc, item) => { if (!acc[item.vendorId]) { acc[item.vendorId] = []; } acc[item.vendorId].push(item); return acc; }, {} as Record); }, [riskDataList]); const [isPending, startTransition] = useTransition(); const form = useForm({ resolver: zodResolver(risksMailFormSchema), defaultValues: { managerId: undefined, adminComment: '', attachment: undefined, }, }); const selectedFile = form.watch('attachment'); const [selectedVendorId, setSelectedVendorId] = useState(riskDataList[0]?.vendorId ?? null); const [selectedCommentType, setSelectedCommentType] = useState(''); const [managerList, setManagerList] = useState[]>([]); const [isLoadingManagerList, setIsLoadingManagerList] = useState(false); const [riskCheckMap, setRiskCheckMap] = useState>({}); useEffect(() => { if (!selectedVendorId) { return; } const eventTypeMap = (riskDataMap[selectedVendorId] || []).reduce>((acc, item) => { if (!acc[item.eventType]) { acc[item.eventType] = []; } acc[item.eventType].push(item); return acc; }, {}); const initialRiskCheckMap: Record = {}; Object.keys(eventTypeMap).forEach((type) => { initialRiskCheckMap[type] = true; }); setRiskCheckMap(initialRiskCheckMap); setSelectedCommentType('기타'); form.reset({ managerId: undefined, adminComment: '', attachment: undefined, }); }, [open, selectedVendorId]); useEffect(() => { if (open) { startTransition(async () => { try { setIsLoadingManagerList(true); form.reset({ managerId: undefined, adminComment: '', attachment: undefined, }); setSelectedCommentType('기타'); const managerList = await getProcurementManagerList(); setManagerList(managerList); } catch (error) { console.error('Error in Loading Risk Event for Managing:', error); toast.error(error instanceof Error ? error.message : '구매 담당자 목록을 불러오는 데 실패했습니다.'); } finally { setIsLoadingManagerList(false); } }); } }, [open, form]); const formatBusinessNumber = (numberString: string) => { if (!numberString) { return '정보 없음'; } return /^\d{10}$/.test(numberString) ? `${numberString.slice(0, 3)}-${numberString.slice(3, 5)}-${numberString.slice(5)}` : numberString; }; const handleCheckboxChange = (type: string) => { if (ALWAYS_CHECKED_TYPES.includes(type)) { return; } setRiskCheckMap(prev => ({ ...prev, [type]: !prev[type], })); }; const handleFileChange = (files: File[]) => { if (files.length === 0) { return; } const file = files[0]; const maxFileSize = 10 * 1024 * 1024 if (file.size > maxFileSize) { toast.error('파일 크기는 10MB를 초과할 수 없습니다.'); return; } form.setValue('attachment', file); form.clearErrors('attachment'); } const removeFile = () => { form.resetField('attachment'); } const onSubmit = async (data: RisksMailFormData) => { startTransition(async () => { try { if (!selectedVendorId) { throw Error('선택된 협력업체가 존재하지 않아요.'); } const newRiskEventData = { managerId: data.managerId , adminComment: data.adminComment, }; await Promise.all( (riskDataMap[selectedVendorId] ?? []).map(riskEvent => modifyRiskEvents(riskEvent.id, newRiskEventData) ) ); const eventTypeMap = (riskDataMap[selectedVendorId] || []).reduce>((acc, item) => { if (!acc[item.eventType]) { acc[item.eventType] = []; } acc[item.eventType].push(item); return acc; }, {}); const filteredEventTypeMap: Record = {}; Object.entries(eventTypeMap).forEach(([type, items]) => { if (ALWAYS_CHECKED_TYPES.includes(type)) { return; } if (riskCheckMap[type]) { filteredEventTypeMap[type] = items; } }); const formData = new FormData(); formData.append('vendorId', String(selectedVendorId)); formData.append('managerId', String(data.managerId)); formData.append('adminComment', data.adminComment); if (data.attachment) { formData.append('attachment', data.attachment); } formData.append('selectedEventTypeMap', JSON.stringify(filteredEventTypeMap)); const res = await fetch('/api/risks/send-risk-email', { method: 'POST', body: formData, }); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.message || '리스크 알림 메일 전송에 실패했습니다.'); } toast.success('리스크 알림 메일이 구매 담당자에게 발송되었습니다.'); onSuccess(); } catch (error) { console.error('Error in Saving Risk Event:', error); toast.error( error instanceof Error ? error.message : '리스크 알림 메일 발송 중 오류가 발생했습니다.', ); } }) } if (!open) { return null; } return ( 리스크 알림 메일 발송 구매 담당자에게 리스크 알림 메일을 발송합니다. setSelectedVendorId(value ? Number(value) : null)} className="mb-4" > {Object.entries(riskDataMap).map(([vendorId, items]) => ( {items[0].vendorName} ))} {Object.entries(riskDataMap).map(([vendorId, items]) => (
협력업체 정보
협력업체명: {items[0].vendorName ?? '정보 없음'}
사업자등록번호: {formatBusinessNumber(items[0].businessNumber ?? '')}
협력업체 코드: {items[0].vendorCode ?? '정보 없음'}
리스크 정보 메일로 전송할 리스크 정보를 선택하세요. {Object.entries( items?.reduce>((acc, item) => { if (!acc[item.eventType]) acc[item.eventType] = []; acc[item.eventType].push(item); return acc; }, {}) || {} ).map(([eventType, groupedItems]) => (
handleCheckboxChange(eventType)} /> {eventType}
{/* Table로 변경할 것 */}
신용평가사
상세 내용
발생일자
{groupedItems.map(item => (
{item.provider}
{item.content}
{format(item.occuredAt, 'yyyy-MM-dd')}
))}
))}
메일 발송 정보
( 구매 담당자 )} />
관리 담당자 의견 {selectedCommentType === '기타' && ( (