Files
mkd/components/hr/VacancyFormModal.tsx
2026-02-04 00:17:04 +05:00

292 lines
16 KiB
TypeScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};