467 lines
26 KiB
TypeScript
467 lines
26 KiB
TypeScript
|
|
|
|||
|
|
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<CandidateFormModalProps> = ({
|
|||
|
|
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<Vacancy[]>(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 (
|
|||
|
|
<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">
|
|||
|
|
<User 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.name}
|
|||
|
|
onChange={(e) => 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
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
|
|||
|
|
<Phone className="w-3 h-3"/> Телефон *
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="tel"
|
|||
|
|
value={formData.phone}
|
|||
|
|
onChange={(e) => 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
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
|
|||
|
|
<Mail className="w-3 h-3"/> Email
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="email"
|
|||
|
|
value={formData.email}
|
|||
|
|
onChange={(e) => 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"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
|
|||
|
|
Вакансия
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
value={formData.vacancyId || ''}
|
|||
|
|
onChange={(e) => setFormData({ ...formData, vacancyId: e.target.value || null })}
|
|||
|
|
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
|
|||
|
|
disabled={loadingVacancies}
|
|||
|
|
>
|
|||
|
|
<option value="">Без вакансии</option>
|
|||
|
|
{availableVacancies.map(vacancy => (
|
|||
|
|
<option key={vacancy.id} value={vacancy.id}>
|
|||
|
|
{vacancy.position} ({vacancy.department})
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
|
|||
|
|
<Briefcase className="w-3 h-3"/> Позиция *
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={formData.position}
|
|||
|
|
onChange={(e) => 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
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
|
|||
|
|
Статус
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
value={formData.stage}
|
|||
|
|
onChange={(e) => setFormData({ ...formData, stage: e.target.value as any })}
|
|||
|
|
className="w-full p-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-primary-500 outline-none"
|
|||
|
|
>
|
|||
|
|
<option value="new">Новый</option>
|
|||
|
|
<option value="interview">Собеседование</option>
|
|||
|
|
<option value="probation">Испытательный срок</option>
|
|||
|
|
<option value="hired">Трудоустроен</option>
|
|||
|
|
<option value="rejected">Отклонен</option>
|
|||
|
|
</select>
|
|||
|
|
</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>
|
|||
|
|
<input
|
|||
|
|
type="url"
|
|||
|
|
value={formData.resumeUrl}
|
|||
|
|
onChange={(e) => 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"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
|
|||
|
|
Сопроводительное письмо
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
value={formData.coverLetter}
|
|||
|
|
onChange={(e) => setFormData({ ...formData, coverLetter: 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>
|
|||
|
|
|
|||
|
|
{/* Собеседование */}
|
|||
|
|
{(formData.stage === 'interview' || formData.stage === 'probation' || formData.stage === 'hired' || candidate?.interviewDate) && (
|
|||
|
|
<div>
|
|||
|
|
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider mb-4 flex items-center gap-2">
|
|||
|
|
<Calendar className="w-4 h-4"/> Собеседование
|
|||
|
|
</h4>
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
|
|||
|
|
Дата собеседования
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="date"
|
|||
|
|
value={formData.interviewDate}
|
|||
|
|
onChange={(e) => setFormData({ ...formData, interviewDate: 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 className="mt-4">
|
|||
|
|
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1 flex items-center gap-1">
|
|||
|
|
<MessageSquare className="w-3 h-3"/> Заметки с собеседования
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
value={formData.interviewNotes}
|
|||
|
|
onChange={(e) => setFormData({ ...formData, interviewNotes: 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>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Предложение */}
|
|||
|
|
{(formData.stage === 'probation' || formData.stage === 'hired' || candidate?.offerDate) && (
|
|||
|
|
<div>
|
|||
|
|
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider mb-4 flex items-center gap-2">
|
|||
|
|
<DollarSign className="w-4 h-4"/> Предложение
|
|||
|
|
</h4>
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
|
|||
|
|
Предложенная зарплата
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={formData.offerSalary}
|
|||
|
|
onChange={(e) => setFormData({ ...formData, offerSalary: e.target.value })}
|
|||
|
|
placeholder="55000"
|
|||
|
|
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>
|
|||
|
|
<input
|
|||
|
|
type="date"
|
|||
|
|
value={formData.offerDate}
|
|||
|
|
onChange={(e) => setFormData({ ...formData, offerDate: 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>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Трудоустройство */}
|
|||
|
|
{formData.stage === 'hired' && (
|
|||
|
|
<div>
|
|||
|
|
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider mb-4 flex items-center gap-2">
|
|||
|
|
<User className="w-4 h-4"/> Трудоустройство
|
|||
|
|
</h4>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
|
|||
|
|
Дата трудоустройства
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="date"
|
|||
|
|
value={formData.hiredDate}
|
|||
|
|
onChange={(e) => setFormData({ ...formData, hiredDate: 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>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Отказ */}
|
|||
|
|
{formData.stage === 'rejected' && (
|
|||
|
|
<div>
|
|||
|
|
<h4 className="text-sm font-black text-slate-400 uppercase tracking-wider mb-4 flex items-center gap-2">
|
|||
|
|
<X className="w-4 h-4"/> Отказ
|
|||
|
|
</h4>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-[10px] text-slate-400 font-bold uppercase mb-1">
|
|||
|
|
Причина отказа
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
value={formData.rejectedReason}
|
|||
|
|
onChange={(e) => setFormData({ ...formData, rejectedReason: 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>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* История событий (только в режиме редактирования) */}
|
|||
|
|
{isEditMode && candidate && (
|
|||
|
|
<div className="pt-6 border-t border-slate-200">
|
|||
|
|
<CandidateEventsTimeline
|
|||
|
|
candidate={{
|
|||
|
|
...candidate,
|
|||
|
|
id: candidate.id
|
|||
|
|
}}
|
|||
|
|
onEventAdded={() => {
|
|||
|
|
// Можно обновить данные кандидата если нужно
|
|||
|
|
}}
|
|||
|
|
onCandidateUpdated={(updatedCandidate) => {
|
|||
|
|
// Обновляем кандидата в форме
|
|||
|
|
// Это обновит отображаемые данные
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</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>
|
|||
|
|
);
|
|||
|
|
};
|