Files
yarmarka/templates/application_detail.html
2026-03-16 18:57:22 +03:00

516 lines
18 KiB
HTML
Raw 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.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Отклик | Rabota.Today</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
body {
background: linear-gradient(145deg, #eef5fa 0%, #e0eaf5 100%);
min-height: 100vh;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: #0b1c34;
color: white;
padding: 20px 40px;
border-radius: 40px;
margin-bottom: 40px;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 28px;
font-weight: 700;
display: flex;
align-items: center;
gap: 15px;
}
.logo i {
color: #3b82f6;
background: rgba(255,255,255,0.1);
padding: 12px;
border-radius: 20px;
}
.nav {
display: flex;
gap: 15px;
align-items: center;
}
.nav a {
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 30px;
}
.nav a:hover {
background: rgba(255,255,255,0.1);
}
.nav .active {
background: #3b82f6;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #4f7092;
text-decoration: none;
}
.application-detail {
background: white;
border-radius: 40px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,20,40,0.1);
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #dee9f5;
flex-wrap: wrap;
gap: 15px;
}
.status-badge {
padding: 8px 20px;
border-radius: 40px;
font-weight: 600;
}
.status-badge.pending { background: #fef3c7; color: #92400e; }
.status-badge.viewed { background: #dbeafe; color: #1e40af; }
.status-badge.accepted { background: #d1fae5; color: #065f46; }
.status-badge.rejected { background: #fee2e2; color: #b91c1c; }
.date {
color: #4f7092;
font-size: 14px;
}
.section {
margin: 30px 0;
padding: 20px;
background: #f9fcff;
border-radius: 20px;
}
.section h3 {
color: #0b1c34;
margin-bottom: 15px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
}
.info-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: white;
border-radius: 15px;
}
.info-item i {
color: #3b82f6;
width: 20px;
}
.message-box {
background: white;
padding: 20px;
border-radius: 20px;
margin: 20px 0;
font-style: italic;
border-left: 4px solid #3b82f6;
}
.response-box {
background: #e6f7e6;
padding: 20px;
border-radius: 20px;
margin: 20px 0;
border-left: 4px solid #10b981;
}
.action-buttons {
display: flex;
gap: 15px;
margin-top: 30px;
}
.btn {
padding: 14px 28px;
border-radius: 40px;
border: none;
font-weight: 600;
cursor: pointer;
}
.btn-primary {
background: #0b1c34;
color: white;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-outline {
background: transparent;
border: 2px solid #dee9f5;
}
.loading {
text-align: center;
padding: 60px;
color: #4f7092;
font-size: 18px;
}
.error-message {
background: #fee2e2;
color: #b91c1c;
padding: 20px;
border-radius: 30px;
text-align: center;
margin: 40px 0;
}
.view-counter {
display: inline-flex;
align-items: center;
gap: 5px;
background: #eef4fa;
padding: 5px 15px;
border-radius: 30px;
font-size: 14px;
color: #1f3f60;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">
<i class="fas fa-briefcase"></i>
Rabota.Today
</div>
<div class="nav" id="nav">
<!-- Навигация будет заполнена динамически -->
</div>
</div>
<a href="/applications" class="back-link"><i class="fas fa-arrow-left"></i> Назад к откликам</a>
<div id="applicationDetail" class="application-detail">
<div class="loading">Загрузка...</div>
</div>
</div>
<script>
const API_BASE_URL = window.location.protocol + '//' + window.location.host + '/api';
const token = localStorage.getItem('accessToken');
const pathParts = window.location.pathname.split('/');
const applicationId = pathParts[pathParts.length - 1];
let currentUser = null; // Объявляем переменную
if (!token) {
window.location.href = '/login';
}
// Функция для обновления навигации
async function updateNavigation() {
const nav = document.getElementById('nav');
if (!currentUser) {
nav.innerHTML = `
<a href="/">Главная</a>
<a href="/vacancies">Вакансии</a>
<a href="/resumes">Резюме</a>
<a href="/login">Войти</a>
`;
return;
}
const firstName = currentUser.full_name ? currentUser.full_name.split(' ')[0] : 'Профиль';
const adminBadge = currentUser.is_admin ? '<span class="admin-badge">Admin</span>' : '';
nav.innerHTML = `
<a href="/">Главная</a>
<a href="/vacancies">Вакансии</a>
<a href="/resumes">Резюме</a>
<a href="/favorites">Избранное</a>
<a href="/applications">Отклики</a>
<a href="/profile" style="background: #3b82f6;">
<i class="fas fa-user-circle"></i> ${firstName} ${adminBadge}
</a>
`;
}
// Загрузка информации о пользователе
async function loadCurrentUser() {
try {
const response = await fetch(`${API_BASE_URL}/user`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
currentUser = await response.json();
await updateNavigation();
}
} catch (error) {
console.error('Error loading user:', error);
}
}
// Загрузка отклика
async function loadApplication() {
try {
console.log('Loading application:', applicationId);
const response = await fetch(`${API_BASE_URL}/applications/${applicationId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Ошибка загрузки');
}
const app = await response.json();
console.log('Application loaded:', app);
renderApplication(app);
} catch (error) {
console.error('Error loading application:', error);
document.getElementById('applicationDetail').innerHTML = `
<div class="error-message">
<i class="fas fa-exclamation-circle"></i>
Ошибка загрузки: ${error.message}
</div>
`;
}
}
// Отображение отклика
function renderApplication(app) {
const container = document.getElementById('applicationDetail');
// Определяем, кто просматривает (работодатель или соискатель)
const isEmployerViewing = currentUser && app.employer_id === currentUser.id;
const isEmployeeViewing = currentUser && app.user_id === currentUser.id;
// Статус на русском
const statusText = {
'pending': 'Ожидает',
'viewed': 'Просмотрено',
'accepted': 'Принято',
'rejected': 'Отклонено'
}[app.status] || app.status;
// Форматирование дат
const createdDate = new Date(app.created_at).toLocaleString('ru-RU');
const viewedDate = app.viewed_at ? new Date(app.viewed_at).toLocaleString('ru-RU') : null;
const responseDate = app.response_at ? new Date(app.response_at).toLocaleString('ru-RU') : null;
container.innerHTML = `
<div class="status-bar">
<span class="status-badge ${app.status}">${statusText}</span>
<span class="date">Создано: ${createdDate}</span>
</div>
<h2 style="margin-bottom: 20px;">${escapeHtml(app.vacancy_title)}</h2>
<!-- Информация о вакансии -->
<div class="section">
<h3>📋 Информация о вакансии</h3>
<div class="info-grid">
<div class="info-item">
<i class="fas fa-building"></i>
<span>${escapeHtml(app.company_name || app.employer_name)}</span>
</div>
<div class="info-item">
<i class="fas fa-money-bill"></i>
<span>${escapeHtml(app.vacancy_salary || 'з/п не указана')}</span>
</div>
<div class="info-item">
<i class="fas fa-envelope"></i>
<span>${escapeHtml(app.employer_email)}</span>
</div>
</div>
</div>
<!-- Информация о соискателе (для работодателя) -->
${isEmployerViewing ? `
<div class="section">
<h3>👤 Информация о соискателе</h3>
<div class="info-grid">
<div class="info-item">
<i class="fas fa-user"></i>
<span>${escapeHtml(app.applicant_name)}</span>
</div>
<div class="info-item">
<i class="fas fa-envelope"></i>
<span>${escapeHtml(app.applicant_email)}</span>
</div>
<div class="info-item">
<i class="fas fa-phone"></i>
<span>${escapeHtml(app.applicant_phone || '—')}</span>
</div>
<div class="info-item">
<i class="fab fa-telegram"></i>
<span>${escapeHtml(app.applicant_telegram || '—')}</span>
</div>
</div>
</div>
` : ''}
<!-- Информация о работодателе (для соискателя) -->
${isEmployeeViewing ? `
<div class="section">
<h3>🏢 Информация о работодателе</h3>
<div class="info-grid">
<div class="info-item">
<i class="fas fa-user"></i>
<span>${escapeHtml(app.employer_name)}</span>
</div>
<div class="info-item">
<i class="fas fa-envelope"></i>
<span>${escapeHtml(app.employer_email)}</span>
</div>
<div class="info-item">
<i class="fab fa-telegram"></i>
<span>${escapeHtml(app.employer_telegram || '—')}</span>
</div>
</div>
</div>
` : ''}
<!-- Сопроводительное письмо -->
${app.message ? `
<div class="message-box">
<strong>✉️ Сопроводительное письмо:</strong>
<p style="margin-top: 10px;">${escapeHtml(app.message)}</p>
</div>
` : ''}
<!-- Ответ работодателя -->
${app.response_message ? `
<div class="response-box">
<strong>💬 Ответ работодателя:</strong>
<p style="margin-top: 10px;">${escapeHtml(app.response_message)}</p>
${responseDate ? `<small style="display: block; margin-top: 10px;">Ответ дан: ${responseDate}</small>` : ''}
</div>
` : ''}
<!-- Информация о просмотре -->
${viewedDate ? `
<div style="color: #4f7092; font-size: 14px; margin-top: 20px; padding: 10px; background: #f9fcff; border-radius: 10px;">
<i class="fas fa-eye"></i> Просмотрено: ${viewedDate}
</div>
` : ''}
<!-- Кнопки для работодателя -->
${isEmployerViewing && app.status === 'pending' ? `
<div class="action-buttons">
<button class="btn btn-success" onclick="updateStatus('accepted')">✅ Принять</button>
<button class="btn btn-danger" onclick="updateStatus('rejected')">❌ Отклонить</button>
</div>
` : ''}
${isEmployerViewing && app.status === 'viewed' ? `
<div class="action-buttons">
<button class="btn btn-success" onclick="updateStatus('accepted')">✅ Принять</button>
<button class="btn btn-danger" onclick="updateStatus('rejected')">❌ Отклонить</button>
</div>
` : ''}
`;
}
// Обновление статуса отклика
async function updateStatus(status) {
const message = prompt('Введите сообщение для соискателя (необязательно):');
try {
const response = await fetch(`${API_BASE_URL}/applications/${applicationId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
status: status,
response_message: message || null
})
});
if (response.ok) {
alert('Статус обновлен');
loadApplication(); // Перезагружаем страницу
} else {
const error = await response.json();
throw new Error(error.detail || 'Ошибка обновления');
}
} catch (error) {
alert('Ошибка: ' + error.message);
}
}
// Экранирование HTML
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Инициализация
async function init() {
await loadCurrentUser();
await loadApplication();
}
init();
</script>
</body>
</html>