import React, { useState, useEffect } from 'react'; import { Vacancy, Candidate } from '../../types'; import { Briefcase, Users, Megaphone, Plus, ExternalLink, Filter, Search, MoreVertical, AlertCircle, Clock, Edit, Trash2, X } from 'lucide-react'; import { VacancyFormModal } from './VacancyFormModal'; import { CandidateFormModal } from './CandidateFormModal'; import { authFetch } from '../../services/apiClient'; export const VacanciesRegistry: React.FC = () => { const [vacancies, setVacancies] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [isFormModalOpen, setIsFormModalOpen] = useState(false); const [editingVacancy, setEditingVacancy] = useState(null); const [selectedVacancy, setSelectedVacancy] = useState(null); const [candidates, setCandidates] = useState([]); const [showCandidates, setShowCandidates] = useState(false); const [isCandidateFormOpen, setIsCandidateFormOpen] = useState(false); const [candidateVacancyId, setCandidateVacancyId] = useState(null); const [editingCandidate, setEditingCandidate] = useState(null); const [error, setError] = useState(null); useEffect(() => { fetchVacancies(); }, []); const fetchVacancies = async () => { try { setLoading(true); setError(null); 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(); setVacancies(data); } else { const errorData = await response.json().catch(() => ({ error: 'Unknown error' })); const errorMessage = errorData.message || errorData.error || `HTTP ${response.status}: ${response.statusText}`; setError(errorMessage); console.error('Error fetching vacancies:', errorMessage, errorData); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to fetch vacancies'; setError(errorMessage); console.error('Error fetching vacancies:', error); } finally { setLoading(false); } }; const fetchCandidates = async (vacancyId: string) => { try { const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; const apiUrl = (import.meta.env.DEV || !apiBaseUrl) ? `/api/vacancies/${vacancyId}/candidates` : `${apiBaseUrl}/vacancies/${vacancyId}/candidates`; const response = await authFetch(apiUrl); if (response.ok) { const data = await response.json(); setCandidates(data); } } catch (error) { console.error('Error fetching candidates:', error); } }; const handleCreateVacancy = () => { setEditingVacancy(null); setIsFormModalOpen(true); }; const handleEditVacancy = (vacancy: Vacancy) => { setEditingVacancy(vacancy); setIsFormModalOpen(true); }; const handleDeleteVacancy = async (vacancy: Vacancy) => { if (!confirm(`Удалить вакансию "${vacancy.position}"?`)) return; try { const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; const apiUrl = (import.meta.env.DEV || !apiBaseUrl) ? `/api/vacancies/${vacancy.id}` : `${apiBaseUrl}/vacancies/${vacancy.id}`; const response = await authFetch(apiUrl, { method: 'DELETE' }); if (response.ok) { await fetchVacancies(); } else { alert('Ошибка при удалении вакансии'); } } catch (error) { console.error('Error deleting vacancy:', error); alert('Ошибка при удалении вакансии'); } }; const handleShowCandidates = async (vacancy: Vacancy) => { setSelectedVacancy(vacancy); await fetchCandidates(vacancy.id); setShowCandidates(true); }; const handleCreateCandidateForVacancy = (vacancy: Vacancy) => { setEditingCandidate(null); setCandidateVacancyId(vacancy.id); setIsCandidateFormOpen(true); }; const handleOpenCandidateCard = (candidate: Candidate) => { setEditingCandidate(candidate); setCandidateVacancyId(selectedVacancy?.id || candidate.vacancyId || null); setShowCandidates(false); setIsCandidateFormOpen(true); }; const handleSaveCandidate = async (candidate: Candidate) => { setIsCandidateFormOpen(false); setCandidateVacancyId(null); setEditingCandidate(null); await fetchVacancies(); // Обновляем список вакансий для обновления счетчика кандидатов if (selectedVacancy) await fetchCandidates(selectedVacancy.id); // обновить список кандидатов, если модалка снова откроют }; const handleSaveVacancy = async (vacancy: Vacancy) => { await fetchVacancies(); setIsFormModalOpen(false); setEditingVacancy(null); }; const filteredVacancies = vacancies.filter(v => { const query = searchQuery.toLowerCase(); return v.position.toLowerCase().includes(query) || v.department.toLowerCase().includes(query) || (v.description && v.description.toLowerCase().includes(query)); }); const activeVacancies = vacancies.filter(v => v.status === 'active' || v.status === 'urgent'); const totalApplicants = vacancies.reduce((sum, v) => sum + (v.applicantsCount || 0), 0); const closedVacancies = vacancies.filter(v => v.status === 'closed').length; const getStatusLabel = (status: string) => { switch (status) { case 'urgent': return 'Срочно'; case 'active': return 'Активна'; case 'paused': return 'Приостановлена'; case 'closed': return 'Закрыта'; default: return status; } }; const getStatusColor = (status: string) => { switch (status) { case 'urgent': return 'bg-red-50 text-red-600 animate-pulse'; case 'active': return 'bg-emerald-50 text-emerald-600'; case 'paused': return 'bg-amber-50 text-amber-600'; case 'closed': return 'bg-slate-50 text-slate-600'; default: return 'bg-slate-50 text-slate-600'; } }; const getStatusLineColor = (status: string) => { switch (status) { case 'urgent': return 'bg-red-500'; case 'active': return 'bg-emerald-500'; case 'paused': return 'bg-amber-500'; case 'closed': return 'bg-slate-400'; default: return 'bg-slate-400'; } }; return (
{/* Vacancy Toolbar */}
setSearchQuery(e.target.value)} className="w-full pl-11 pr-4 py-3 bg-white border border-slate-200 rounded-2xl text-sm outline-none focus:ring-2 focus:ring-primary-500 shadow-sm" />
{/* Stats Header */}
Рекрутинг УК

Открытые позиции

Всего {activeVacancies.length} активных вакансий. Текущий срок закрытия позиции в среднем: 14 дней.

Всего откликов

{totalApplicants}

В архиве

{closedVacancies}

{/* Кнопка создания вакансии - над списком */} {/* Error Message */} {error && (

Ошибка загрузки вакансий

{error}

)} {/* List of Vacancies */} {loading ? (
Загрузка...
) : error ? null : filteredVacancies.length === 0 ? (
{searchQuery ? 'Вакансии не найдены' : 'Нет вакансий. Создайте первую вакансию.'}
) : (
{filteredVacancies.map(vacancy => (
{/* Status Vertical Line */}
{getStatusLabel(vacancy.status)} {vacancy.department} Опубликовано: {vacancy.postedDate}

{vacancy.position}

{vacancy.description}

{vacancy.salary && (

{vacancy.salary}

)}
{vacancy.applicantsCount || 0} откликов
{/* Posting Actions */}
))}
)}

Уровень зарплат по рабочим вакансиям (сантехники, электрики) вырос на 15% в регионе за последний квартал. Рекомендуем пересмотреть вилки для ускорения найма.

{/* Модальное окно создания/редактирования вакансии */} {isFormModalOpen && ( { setIsFormModalOpen(false); setEditingVacancy(null); }} onSave={handleSaveVacancy} /> )} {/* Модальное окно создания/редактирования кандидата */} {isCandidateFormOpen && ( { setIsCandidateFormOpen(false); setCandidateVacancyId(null); setEditingCandidate(null); }} onSave={handleSaveCandidate} /> )} {/* Модальное окно кандидатов */} {showCandidates && selectedVacancy && (
setShowCandidates(false)} >
e.stopPropagation()} >

Кандидаты

{selectedVacancy.position}

{candidates.length === 0 ? (
Нет кандидатов на эту вакансию
) : (
{candidates.map(candidate => ( ))}
)}
)}
); };