'use client'; import React, { useState, useEffect, useMemo } from 'react'; import { Search, Plus, X, ListChecks } from 'lucide-react'; import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; import TicketCard from '@/components/issues/KanbanBoard'; import { KanbanColumn } from '@/components/issues/KanbanBoard'; import ticketService from '@/services/ticketService'; import apiClient from '@/services/apiClient'; import type { IssueTicket, TicketCategory, TicketPriority, Project } from '@/types'; import { useCommonCodes } from '@/hooks/useCommonCodes'; const columns = [ { key: 'OPEN', label: 'Open', color: 'bg-status-open-text' }, { key: 'ASSIGNED', label: 'Assigned', color: 'bg-status-pending-text' }, { key: 'IN_PROGRESS', label: 'In Progress', color: 'bg-status-pending-text' }, { key: 'PENDING_APPROVAL', label: 'Pending Approval', color: 'bg-amber-500' }, { key: 'APPROVED', label: 'Approved', color: 'bg-emerald-500' }, { key: 'REJECTED', label: 'Rejected', color: 'bg-red-500' }, { key: 'DONE', label: 'Done', color: 'bg-status-resolved-text' }, ]; function CreateTicketModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) { const [formData, setFormData] = useState({ title: '', description: '', category: 'BUG', priority: 'NORMAL', projectId: '', }); const [extraFields, setExtraFields] = useState>({}); const [projects, setProjects] = useState([]); const [approvers, setApprovers] = useState<{empId: number; name: string; email: string}[]>([]); const [submitting, setSubmitting] = useState(false); const [fieldErrors, setFieldErrors] = useState>({}); useEffect(() => { apiClient.get<{ success: boolean; data: Project[] }>('/projects') .then((res) => setProjects(res.data)) .catch(console.error); apiClient.get<{ success: boolean; data: {empId: number; name: string; email: string}[] }>('/employees/approvers') .then((res) => setApprovers(res.data)) .catch(console.error); }, []); const updateField = (field: string, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); if (fieldErrors[field]) { setFieldErrors((prev) => { const next = { ...prev }; delete next[field]; return next; }); } }; const updateExtraField = (field: string, value: string) => { setExtraFields((prev) => ({ ...prev, [field]: value })); if (fieldErrors[field]) { setFieldErrors((prev) => { const next = { ...prev }; delete next[field]; return next; }); } }; const needsApprover = ['VACATION', 'EXPENSE', 'EQUIPMENT_REQUEST', 'GENERAL'].includes(formData.category); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const errors: Record = {}; if (!formData.title.trim()) errors.title = '제목을 입력해주세요'; if (!formData.description.trim()) errors.description = '내용을 입력해주세요'; if (formData.category === 'VACATION') { if (!extraFields.startDate) errors.startDate = '시작일을 선택해주세요'; if (!extraFields.endDate) errors.endDate = '종료일을 선택해주세요'; } if (formData.category === 'EXPENSE') { if (!extraFields.amount) errors.amount = '금액을 입력해주세요'; } if (formData.category === 'EQUIPMENT_REQUEST') { if (!extraFields.equipmentType?.trim()) errors.equipmentType = '장비 유형을 입력해주세요'; if (!extraFields.urgency) errors.urgency = '긴급도를 선택해주세요'; } if (needsApprover && !extraFields.approverId) { errors.approverId = '승인자를 선택해주세요'; } if (Object.keys(errors).length > 0) { setFieldErrors(errors); return; } setSubmitting(true); try { let finalDescription = formData.description; const extras: string[] = []; if (formData.category === 'VACATION') { extras.push(`\n\n[휴가 신청]\n시작일: ${extraFields.startDate}\n종료일: ${extraFields.endDate}`); } if (formData.category === 'EXPENSE') { extras.push(`\n\n[비용 청구]\n금액: ${extraFields.amount}원${extraFields.receiptUrl ? `\n영수증: ${extraFields.receiptUrl}` : ''}`); } if (formData.category === 'EQUIPMENT_REQUEST') { extras.push(`\n\n[장비 요청]\n장비 유형: ${extraFields.equipmentType}\n긴급도: ${extraFields.urgency}`); } if (extras.length > 0) { finalDescription += extras.join(''); } await ticketService.createTicket({ title: formData.title, description: finalDescription, category: formData.category as TicketCategory, priority: formData.priority as TicketPriority, projectId: formData.projectId ? parseInt(formData.projectId) : undefined, attachmentUrl: extraFields.receiptUrl || undefined, attachmentName: extraFields.receiptName || undefined, approverId: extraFields.approverId ? parseInt(extraFields.approverId) : undefined, backupApproverId: extraFields.backupApproverId ? parseInt(extraFields.backupApproverId) : undefined, }); onSuccess(); onClose(); } catch (err: any) { setFieldErrors({ submit: err.message || '티켓 생성에 실패했습니다' }); } finally { setSubmitting(false); } }; const renderFieldError = (field: string) => { if (!fieldErrors[field]) return null; return

{fieldErrors[field]}

; }; return (

New SR Ticket

{fieldErrors.submit && (
{fieldErrors.submit}
)}
updateField('title', e.target.value)} className={`w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary ${ fieldErrors.title ? 'border-error' : 'border-border' }`} placeholder="Brief description of the issue" /> {renderFieldError('title')}
{/* Project: only for project-related categories */} {['BUG', 'FEATURE', 'SUPPORT', 'QA'].includes(formData.category) && (
)} {formData.category === 'VACATION' && (
updateExtraField('startDate', e.target.value)} className={`w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary ${ fieldErrors.startDate ? 'border-error' : 'border-border' }`} /> {renderFieldError('startDate')}
updateExtraField('endDate', e.target.value)} className={`w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary ${ fieldErrors.endDate ? 'border-error' : 'border-border' }`} /> {renderFieldError('endDate')}
)} {formData.category === 'EXPENSE' && ( <>
updateExtraField('amount', e.target.value)} className={`w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary ${ fieldErrors.amount ? 'border-error' : 'border-border' }`} placeholder="0" /> {renderFieldError('amount')}
{ const file = e.target.files?.[0]; if (!file) return; updateExtraField('receiptName', file.name); try { const form = new FormData(); form.append('file', file); const token = localStorage.getItem('accessToken'); const res = await fetch('/api/v1/files/upload', { method: 'POST', headers: token ? {Authorization: `Bearer ${token}`} : {}, body: form, }); const json = await res.json(); if (json.success) updateExtraField('receiptUrl', json.data); } catch { updateExtraField('receiptUrl', ''); } }} className="w-full text-sm" /> {extraFields.receiptName && (

📎 {extraFields.receiptName}

)}
)} {formData.category === 'EQUIPMENT_REQUEST' && (
updateExtraField('equipmentType', e.target.value)} className={`w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary ${ fieldErrors.equipmentType ? 'border-error' : 'border-border' }`} placeholder="예: 노트북, 모니터" /> {renderFieldError('equipmentType')}
{renderFieldError('urgency')}
)} {/* Approver dropdowns for VACATION/EXPENSE/EQUIPMENT_REQUEST/GENERAL */} {needsApprover && (
{renderFieldError('approverId')}
)}