import React, { useState, useEffect } from 'react'; import { Candidate, Vacancy } from '../../types'; import { X, User, Phone, Mail, FileText, Briefcase, Calendar, MessageSquare, DollarSign } from 'lucide-react'; import { CandidateEventsTimeline } from './CandidateEventsTimeline'; import { authFetch } from '../../services/apiClient'; interface CandidateFormModalProps { candidate?: Candidate | null; vacancyId?: string | null; vacancies?: Vacancy[]; onClose: () => void; onSave: (candidate: Candidate) => void; } export const CandidateFormModal: React.FC = ({ candidate, vacancyId: initialVacancyId, vacancies = [], onClose, onSave }) => { const isEditMode = !!candidate; const [formData, setFormData] = useState({ name: candidate?.name || '', position: candidate?.position || '', vacancyId: candidate?.vacancyId || initialVacancyId || '', stage: candidate?.stage || 'new' as 'new' | 'interview' | 'probation' | 'hired' | 'rejected', phone: candidate?.phone || '', email: candidate?.email || '', resumeUrl: candidate?.resumeUrl || '', coverLetter: candidate?.coverLetter || '', interviewDate: candidate?.interviewDate ? new Date(candidate.interviewDate).toISOString().split('T')[0] : '', interviewNotes: candidate?.interviewNotes || '', offerSalary: candidate?.offerSalary?.toString() || '', offerDate: candidate?.offerDate || '', hiredDate: candidate?.hiredDate || '', rejectedReason: candidate?.rejectedReason || '', }); const [loadingVacancies, setLoadingVacancies] = useState(false); const [availableVacancies, setAvailableVacancies] = useState(vacancies); // Загружаем вакансии если не переданы useEffect(() => { if (vacancies.length === 0) { const fetchVacancies = async () => { try { setLoadingVacancies(true); const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; const apiUrl = (import.meta.env.DEV || !apiBaseUrl) ? '/api/vacancies' : `${apiBaseUrl}/vacancies`; const response = await authFetch(apiUrl); if (response.ok) { const data = await response.json(); setAvailableVacancies(data); } } catch (error) { console.error('Error fetching vacancies:', error); } finally { setLoadingVacancies(false); } }; fetchVacancies(); } }, [vacancies.length]); // Обновляем позицию при выборе вакансии useEffect(() => { if (formData.vacancyId) { const selectedVacancy = availableVacancies.find(v => v.id === formData.vacancyId); if (selectedVacancy && !isEditMode) { setFormData(prev => ({ ...prev, position: selectedVacancy.position })); } } }, [formData.vacancyId, availableVacancies, isEditMode]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.name || !formData.position || !formData.phone) { alert('Заполните обязательные поля: ФИО, позиция и телефон'); return; } try { const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; const apiUrl = (import.meta.env.DEV || !apiBaseUrl) ? `/api/candidates${isEditMode ? `/${candidate.id}` : ''}` : `${apiBaseUrl}/candidates${isEditMode ? `/${candidate.id}` : ''}`; const method = isEditMode ? 'PUT' : 'POST'; const body = { ...(isEditMode ? {} : { id: `cand-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` }), name: formData.name.trim(), position: formData.position.trim(), vacancyId: formData.vacancyId && formData.vacancyId.trim() ? formData.vacancyId.trim() : null, stage: formData.stage, phone: formData.phone.trim(), email: formData.email.trim() || null, resumeUrl: formData.resumeUrl.trim() || null, coverLetter: formData.coverLetter.trim() || null, interviewDate: formData.interviewDate || null, interviewNotes: formData.interviewNotes.trim() || null, offerSalary: formData.offerSalary ? parseFloat(formData.offerSalary) : null, offerDate: formData.offerDate || null, hiredDate: formData.hiredDate || null, rejectedReason: formData.rejectedReason.trim() || null, }; const response = await authFetch(apiUrl, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!response.ok) { let errorMessage = `Ошибка ${response.status}: ${response.statusText}`; try { const text = await response.text(); if (text) { try { const errorData = JSON.parse(text); errorMessage = errorData.error || errorData.message || text; } catch { errorMessage = text; } } } catch (textError) { console.error('Failed to read error response:', textError); } throw new Error(errorMessage); } const savedCandidate = await response.json(); onSave(savedCandidate); } catch (error) { console.error('Error saving candidate:', error); let errorMessage = 'Ошибка при сохранении кандидата'; if (error instanceof Error) { errorMessage = error.message; if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) { errorMessage = 'Ошибка сети. Проверьте подключение к серверу.'; } } alert(errorMessage); } }; const getStageLabel = (stage: string) => { switch (stage) { case 'new': return 'Новый'; case 'interview': return 'Собеседование'; case 'probation': return 'Испытательный срок'; case 'hired': return 'Трудоустроен'; case 'rejected': return 'Отклонен'; default: return stage; } }; return (
e.stopPropagation()} > {/* Header */}

{isEditMode ? 'Редактирование кандидата' : 'Создание нового кандидата'}

{/* Form */}
{/* Основная информация */}

Основная информация

setFormData({ ...formData, name: e.target.value })} placeholder="Иванов Иван Иванович" className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none" required />
setFormData({ ...formData, phone: e.target.value })} placeholder="+7 (999) 123-45-67" className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none" required />
setFormData({ ...formData, email: e.target.value })} placeholder="ivanov@example.com" className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none" />
setFormData({ ...formData, position: e.target.value })} placeholder="Слесарь-сантехник" className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none" required />
{/* Документы и материалы */}

Документы и материалы

setFormData({ ...formData, resumeUrl: e.target.value })} placeholder="https://..." className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none" />