Files
mkd/components/hr/VacancyFormModal.tsx

292 lines
16 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
import React, { useState } from 'react';
import { Vacancy } from '../../types';
import { X, Briefcase, DollarSign, FileText, Users, Calendar, AlertCircle } from 'lucide-react';
import { authFetch } from '../../services/apiClient';
interface VacancyFormModalProps {
vacancy?: Vacancy | null;
onClose: () => void;
onSave: (vacancy: Vacancy) => void;
}
export const VacancyFormModal: React.FC<VacancyFormModalProps> = ({ vacancy, onClose, onSave }) => {
const isEditMode = !!vacancy;
const [formData, setFormData] = useState({
position: vacancy?.position || '',
department: vacancy?.department || '',
status: vacancy?.status || 'active' as 'urgent' | 'active' | 'paused' | 'closed',
salary: vacancy?.salary || '',
description: vacancy?.description || '',
requirements: vacancy?.requirements || '',
conditions: vacancy?.conditions || '',
responsibilities: vacancy?.responsibilities || '',
postedDate: vacancy?.postedDate || new Date().toISOString().split('T')[0],
closingDate: vacancy?.closingDate || '',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.position || !formData.department || !formData.description) {
alert('Заполните обязательные поля: позиция, отдел и описание');
return;
}
try {
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const apiUrl = (import.meta.env.DEV || !apiBaseUrl)
? `/api/vacancies${isEditMode ? `/${vacancy.id}` : ''}`
: `${apiBaseUrl}/vacancies${isEditMode ? `/${vacancy.id}` : ''}`;
const method = isEditMode ? 'PUT' : 'POST';
const body = {
...(isEditMode ? {} : { id: `vac-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` }),
position: formData.position.trim(),
department: formData.department.trim(),
status: formData.status,
salary: formData.salary.trim() || null,
description: formData.description.trim(),
requirements: formData.requirements.trim() || null,
conditions: formData.conditions.trim() || null,
responsibilities: formData.responsibilities.trim() || null,
postedDate: formData.postedDate,
closingDate: formData.closingDate.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 {
// Сначала читаем как текст, затем пытаемся распарсить как JSON
const text = await response.text();
if (text) {
try {
const errorData = JSON.parse(text);
errorMessage = errorData.error || errorData.message || text;
} catch {
// Если не JSON, используем текст как есть
errorMessage = text;
}
}
} catch (textError) {
// Если не удалось прочитать ответ, используем дефолтное сообщение
console.error('Failed to read error response:', textError);
}
throw new Error(errorMessage);
}
const savedVacancy = await response.json();
onSave(savedVacancy);
} catch (error) {
console.error('Error saving vacancy:', error);
let errorMessage = 'Ошибка при сохранении вакансии';
if (error instanceof Error) {
errorMessage = error.message;
// Проверяем на сетевые ошибки
if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
errorMessage = 'Ошибка сети. Проверьте подключение к серверу.';
}
}
alert(errorMessage);
}
};
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in"
onClick={onClose}
>
<div
className="bg-white rounded-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto shadow-2xl animate-slide-up"
onClick={e => e.stopPropagation()}
>
{/* Header */}
<div className="sticky top-0 bg-white border-b border-slate-200 p-6 rounded-t-2xl z-10">
<div className="flex justify-between items-center">
<h3 className="text-2xl font-bold text-slate-800">
{isEditMode ? 'Редактирование вакансии' : 'Создание новой вакансии'}
</h3>
<button
onClick={onClose}
className="p-2 hover:bg-slate-100 rounded-full transition-colors"
>
<X className="w-5 h-5 text-slate-500"/>
</button>
</div>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* Основная информация */}
<div>
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider mb-4 flex items-center gap-2">
<Briefcase className="w-4 h-4"/> Основная информация
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
Название должности *
</label>
<input
type="text"
value={formData.position}
onChange={(e) => setFormData({ ...formData, position: e.target.value })}
placeholder="Слесарь-сантехник (4-5 разряд)"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
required
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
Отдел *
</label>
<input
type="text"
value={formData.department}
onChange={(e) => setFormData({ ...formData, department: 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
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
<DollarSign className="w-3 h-3"/> Зарплата
</label>
<input
type="text"
value={formData.salary}
onChange={(e) => setFormData({ ...formData, salary: e.target.value })}
placeholder="55 000 - 65 000 ₽"
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
Статус
</label>
<select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as 'urgent' | 'active' | 'paused' | 'closed' })}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
>
<option value="active">Активна</option>
<option value="urgent">Срочно</option>
<option value="paused">Приостановлена</option>
<option value="closed">Закрыта</option>
</select>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
<Calendar className="w-3 h-3"/> Дата публикации
</label>
<input
type="date"
value={formData.postedDate}
onChange={(e) => setFormData({ ...formData, postedDate: e.target.value })}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
<Calendar className="w-3 h-3"/> Дата закрытия
</label>
<input
type="date"
value={formData.closingDate}
onChange={(e) => setFormData({ ...formData, closingDate: e.target.value })}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
</div>
</div>
{/* Описание */}
<div>
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider mb-4 flex items-center gap-2">
<FileText className="w-4 h-4"/> Описание вакансии
</h4>
<div className="space-y-4">
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
Описание *
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Краткое описание вакансии..."
rows={3}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
required
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
Обязанности
</label>
<textarea
value={formData.responsibilities}
onChange={(e) => setFormData({ ...formData, responsibilities: e.target.value })}
placeholder="Основные обязанности сотрудника..."
rows={4}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
Требования к кандидату
</label>
<textarea
value={formData.requirements}
onChange={(e) => setFormData({ ...formData, requirements: e.target.value })}
placeholder="Образование, опыт работы, навыки..."
rows={4}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
Условия работы
</label>
<textarea
value={formData.conditions}
onChange={(e) => setFormData({ ...formData, conditions: e.target.value })}
placeholder="График работы, режим, дополнительные условия..."
rows={3}
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
</div>
</div>
{/* Кнопки */}
<div className="flex gap-3 pt-4 border-t border-slate-200">
<button
type="button"
onClick={onClose}
className="flex-1 py-3 bg-slate-50 text-slate-700 rounded-xl text-xs font-black uppercase hover:bg-slate-100 transition-all"
>
Отмена
</button>
<button
type="submit"
className="flex-1 py-3 bg-primary-600 text-white rounded-xl text-xs font-black uppercase shadow-lg shadow-primary-500/20 hover:bg-primary-700 active:scale-95 transition-all"
>
{isEditMode ? 'Сохранить' : 'Создать вакансию'}
</button>
</div>
</form>
</div>
</div>
);
};