Files
yarmarka/templates/profile.html
2026-03-19 19:29:30 +03:00

2435 lines
88 KiB
HTML
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.
<!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">
<!-- Подключаем библиотеку для генерации QR-кодов -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.1/build/qrcode.min.js"></script>
<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: 1200px;
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;
flex-wrap: wrap;
gap: 20px;
}
.logo {
font-size: 28px;
font-weight: 700;
display: flex;
align-items: center;
gap: 15px;
cursor: pointer;
}
.logo i {
color: #3b82f6;
background: rgba(255,255,255,0.1);
padding: 12px;
border-radius: 20px;
}
.nav {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.nav a {
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 30px;
transition: 0.2s;
}
.nav a:hover {
background: rgba(255,255,255,0.1);
}
.nav .active {
background: #3b82f6;
}
.profile-link {
display: flex;
align-items: center;
gap: 8px;
background: #3b82f6;
padding: 8px 20px !important;
}
.profile-grid {
display: grid;
grid-template-columns: 300px 1fr;
gap: 30px;
}
.profile-sidebar {
background: white;
border-radius: 40px;
padding: 30px;
text-align: center;
box-shadow: 0 20px 40px rgba(0,20,40,0.1);
height: fit-content;
}
.avatar {
width: 120px;
height: 120px;
background: #eef4fa;
border-radius: 60px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
font-size: 48px;
color: #3b82f6;
}
.profile-name {
font-size: 24px;
font-weight: 700;
color: #0b1c34;
margin-bottom: 5px;
}
.profile-role {
color: #3b82f6;
font-weight: 600;
margin-bottom: 20px;
}
.profile-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin: 20px 0;
padding: 20px 0;
border-top: 1px solid #dee9f5;
border-bottom: 1px solid #dee9f5;
}
.stat-card {
background: #f9fcff;
border-radius: 20px;
padding: 15px;
text-align: center;
cursor: pointer;
transition: 0.2s;
text-decoration: none;
color: inherit;
display: block;
}
.stat-card:hover {
background: #eef4fa;
transform: translateY(-2px);
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #0b1c34;
}
.stat-label {
font-size: 12px;
color: #4f7092;
margin-top: 5px;
}
.contact-info {
text-align: left;
margin-top: 20px;
}
.contact-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
color: #1f3f60;
}
.contact-item i {
color: #3b82f6;
width: 20px;
}
.profile-content {
background: white;
border-radius: 40px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0,20,40,0.1);
}
.content-tabs {
display: flex;
gap: 10px;
margin-bottom: 30px;
border-bottom: 2px solid #dee9f5;
padding-bottom: 15px;
flex-wrap: wrap;
}
.content-tab {
padding: 10px 20px;
cursor: pointer;
font-weight: 600;
color: #4f7092;
border-radius: 30px;
transition: 0.2s;
}
.content-tab:hover {
background: #eef4fa;
}
.content-tab.active {
background: #eef4fa;
color: #0b1c34;
}
.content-pane {
display: none;
}
.content-pane.active {
display: block;
}
.btn {
padding: 12px 24px;
border-radius: 30px;
border: none;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: 0.2s;
text-decoration: none;
}
.btn-primary {
background: #0b1c34;
color: white;
}
.btn-primary:hover {
background: #1b3f6b;
}
.btn-outline {
background: transparent;
border: 2px solid #3b82f6;
color: #0b1c34;
}
.btn-outline:hover {
background: #eef4fa;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
}
.btn-info {
background: #3b82f6;
color: white;
}
.btn-info:hover {
background: #2563eb;
}
.action-buttons {
display: flex;
gap: 15px;
margin: 20px 0;
flex-wrap: wrap;
}
/* Стили для QR-кнопки */
.qr-profile-button {
width: 44px;
height: 44px;
background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);
border-radius: 22px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(59,130,246,0.3);
color: white;
font-size: 20px;
position: relative;
overflow: hidden;
border: none;
margin-left: 10px;
}
.qr-profile-button:hover {
transform: translateY(-3px) scale(1.05);
box-shadow: 0 8px 20px rgba(59,130,246,0.4);
}
.qr-profile-button:active {
transform: scale(0.95);
}
.qr-profile-button i {
font-size: 20px;
}
/* Стили для вакансий */
.vacancies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.vacancy-card {
background: #f9fcff;
border: 1px solid #dee9f5;
border-radius: 30px;
padding: 25px;
transition: 0.2s;
position: relative;
}
.vacancy-card:hover {
background: white;
box-shadow: 0 10px 30px rgba(0,20,40,0.1);
}
.vacancy-card h3 {
color: #0b1c34;
margin-bottom: 10px;
font-size: 18px;
padding-right: 35px;
}
.vacancy-salary {
color: #3b82f6;
font-weight: 700;
font-size: 18px;
margin: 10px 0;
}
.vacancy-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin: 10px 0;
}
.tag {
background: #eef4fa;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
color: #1f3f60;
}
.vacancy-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
font-size: 14px;
color: #4f7092;
}
/* Стили для меню с тремя точками */
.vacancy-menu {
position: absolute;
top: 15px;
right: 15px;
z-index: 100;
}
.menu-trigger {
background: white;
border: 1px solid #dee9f5;
width: 36px;
height: 36px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: 0.2s;
color: #4f7092;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.menu-trigger:hover {
background: #f0f7ff;
color: #0b1c34;
transform: scale(1.05);
}
.menu-dropdown {
position: absolute;
top: 40px;
right: 0;
background: white;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
min-width: 200px;
display: none;
overflow: hidden;
z-index: 200;
border: 1px solid #eef4fa;
}
.menu-dropdown.active {
display: block;
animation: fadeIn 0.2s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.menu-item {
padding: 12px 16px;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
transition: 0.2s;
color: #1f3f60;
font-size: 14px;
border-bottom: 1px solid #eef4fa;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:hover {
background: #f0f7ff;
}
.menu-item i {
width: 18px;
font-size: 14px;
}
.menu-item.view i { color: #3b82f6; }
.menu-item.edit i { color: #f59e0b; }
.menu-item.activate i { color: #10b981; }
.menu-item.deactivate i { color: #f97316; }
.menu-item.delete i { color: #ef4444; }
.menu-divider {
height: 1px;
background: #eef4fa;
margin: 4px 0;
}
.menu-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
z-index: 90;
}
.menu-overlay.active {
display: block;
}
/* Статус бейдж */
.status-badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 30px;
font-size: 11px;
font-weight: 600;
margin-left: 8px;
}
.status-badge.active {
background: #d1fae5;
color: #065f46;
}
.status-badge.inactive {
background: #fee2e2;
color: #b91c1c;
}
/* Фильтры для вакансий */
.filter-tabs {
display: flex;
gap: 10px;
background: #f0f7ff;
padding: 5px;
border-radius: 40px;
max-width: 400px;
margin-bottom: 15px;
}
.filter-tab {
flex: 1;
border: none;
background: transparent;
padding: 10px 16px;
border-radius: 40px;
font-weight: 600;
font-size: 14px;
color: #385073;
cursor: pointer;
transition: 0.2s;
white-space: nowrap;
}
.filter-tab.active {
background: white;
color: #0b1c34;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.filter-tab:hover:not(.active) {
background: rgba(255,255,255,0.5);
}
.vacancy-stats {
display: flex;
gap: 15px;
margin-top: 15px;
font-size: 14px;
color: #4f7092;
}
/* Стили для резюме */
.resume-section {
background: #f9fcff;
border-radius: 30px;
padding: 30px;
}
.resume-field {
margin-bottom: 20px;
}
.resume-field label {
display: block;
font-weight: 600;
color: #1f3f60;
margin-bottom: 5px;
}
.resume-field input,
.resume-field textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #dee9f5;
border-radius: 20px;
font-size: 16px;
}
.exp-item, .edu-item {
background: white;
border-radius: 20px;
padding: 20px;
margin-bottom: 15px;
border: 1px solid #dee9f5;
}
.item-actions {
display: flex;
justify-content: flex-end;
margin-top: 15px;
}
.resume-preview-link {
display: inline-flex;
align-items: center;
gap: 8px;
background: #eef4fa;
padding: 10px 20px;
border-radius: 30px;
color: #0b1c34;
text-decoration: none;
margin-bottom: 20px;
font-weight: 600;
}
.resume-preview-link:hover {
background: #dbeafe;
}
/* Стили для откликов */
.applications-summary {
background: #f9fcff;
border-radius: 20px;
padding: 20px;
margin-bottom: 20px;
}
.applications-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin: 15px 0;
}
.stat-item {
text-align: center;
padding: 10px;
background: white;
border-radius: 15px;
}
.stat-item .number {
font-size: 20px;
font-weight: 700;
color: #0b1c34;
}
.stat-item .label {
font-size: 12px;
color: #4f7092;
}
.stat-item.pending .number { color: #f59e0b; }
.stat-item.viewed .number { color: #3b82f6; }
.stat-item.accepted .number { color: #10b981; }
.stat-item.rejected .number { color: #ef4444; }
/* Модальное окно для QR */
.qr-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
align-items: flex-start;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(5px);
overflow-y: auto;
padding: 20px 0;
}
.qr-modal.active {
display: flex;
animation: fadeIn 0.3s;
}
.qr-modal-content {
background: white;
border-radius: 40px;
padding: 40px;
max-width: 500px;
width: 90%;
position: relative;
box-shadow: 0 30px 60px rgba(0,0,0,0.3);
animation: slideUp 0.3s;
margin: auto;
max-height: 90vh;
overflow-y: auto;
}
.qr-modal-close {
position: sticky;
top: 0;
right: 0;
margin-left: auto;
margin-bottom: 20px;
width: 40px;
height: 40px;
border-radius: 20px;
background: #eef4fa;
border: none;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #4f7092;
transition: 0.2s;
z-index: 10;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.qr-modal-close:hover {
background: #dbeafe;
color: #0b1c34;
transform: scale(1.1);
}
.qr-modal h2 {
color: #0b1c34;
margin-bottom: 10px;
font-size: 24px;
display: flex;
align-items: center;
gap: 10px;
word-break: break-word;
padding-right: 40px;
position: sticky;
top: 0;
background: white;
padding-top: 0;
z-index: 5;
}
.qr-modal h2 i {
color: #3b82f6;
}
.qr-subtitle {
color: #4f7092;
margin-bottom: 25px;
font-size: 16px;
word-break: break-all;
}
.qr-container {
display: flex;
justify-content: center;
margin: 30px 0;
padding: 20px;
background: #f9fcff;
border-radius: 30px;
position: relative;
}
#qrProfileCanvas {
width: 250px;
height: 250px;
image-rendering: crisp-edges;
display: block;
}
.qr-logo-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
background: white;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
border: 3px solid white;
pointer-events: none;
}
.qr-logo-overlay img {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 12px;
}
.qr-logo-overlay i {
font-size: 30px;
color: #3b82f6;
}
.qr-stats {
display: flex;
justify-content: space-around;
margin: 20px 0;
padding: 15px;
background: #f0f7ff;
border-radius: 20px;
flex-wrap: wrap;
gap: 10px;
}
.qr-stat-item {
text-align: center;
min-width: 80px;
}
.qr-stat-value {
font-size: 20px;
font-weight: 700;
color: #0b1c34;
}
.qr-stat-label {
font-size: 12px;
color: #4f7092;
}
.qr-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin: 20px 0;
}
.qr-action-btn {
padding: 14px;
border-radius: 30px;
border: none;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: 0.2s;
font-size: 14px;
}
.qr-action-btn.primary {
background: #0b1c34;
color: white;
}
.qr-action-btn.primary:hover {
background: #1b3f6b;
}
.qr-action-btn.secondary {
background: #eef4fa;
color: #1f3f60;
}
.qr-action-btn.secondary:hover {
background: #dbeafe;
}
.qr-action-btn i {
font-size: 16px;
}
.qr-share-buttons {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #dee9f5;
flex-wrap: wrap;
}
.qr-share-btn {
width: 44px;
height: 44px;
border-radius: 22px;
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
cursor: pointer;
transition: 0.2s;
}
.qr-share-btn.whatsapp {
background: #25D366;
color: white;
}
.qr-share-btn.telegram {
background: #0088cc;
color: white;
}
.qr-share-btn.email {
background: #ea4335;
color: white;
}
.qr-share-btn:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.loading {
text-align: center;
padding: 40px;
color: #4f7092;
grid-column: 1/-1;
}
.error-message {
background: #fee2e2;
color: #b91c1c;
padding: 20px;
border-radius: 30px;
text-align: center;
margin: 20px 0;
}
.admin-badge {
background: #f59e0b;
color: white;
padding: 2px 8px;
border-radius: 20px;
font-size: 12px;
margin-left: 5px;
}
/* Уведомления */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 16px 24px;
border-radius: 30px;
background: white;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
z-index: 9999;
animation: slideIn 0.3s;
max-width: 350px;
display: none;
}
.notification.success {
background: #10b981;
color: white;
}
.notification.error {
background: #ef4444;
color: white;
}
.notification.info {
background: #3b82f6;
color: white;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0%); opacity: 1; }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@media (max-width: 768px) {
.profile-grid {
grid-template-columns: 1fr;
}
.applications-stats {
grid-template-columns: repeat(2, 1fr);
}
.filter-tabs {
max-width: 100%;
}
.qr-modal-content {
padding: 30px 20px;
width: 95%;
}
.qr-modal-close {
position: fixed;
top: 10px;
right: 10px;
background: white;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
}
</style>
</head>
<body>
<div class="container">
<!-- Шапка -->
<div class="header">
<div class="logo" onclick="window.location.href='/'">
<i class="fas fa-briefcase"></i>
Rabota.Today
</div>
<div class="nav" id="nav">
<div class="loading" style="color: white; padding: 0;">Загрузка...</div>
</div>
</div>
<!-- Профиль -->
<div class="profile-grid">
<!-- Боковая панель -->
<div class="profile-sidebar">
<div class="avatar">
<i class="fas fa-user-circle"></i>
</div>
<div style="display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 10px;">
<div class="profile-name" id="profileName">Загрузка...</div>
<button class="qr-profile-button" onclick="openProfileQRModal()" title="QR-код профиля">
<i class="fas fa-qrcode"></i>
</button>
</div>
<div class="profile-role" id="profileRole"></div>
<div class="profile-stats">
<a href="/favorites" class="stat-card">
<div class="stat-value" id="totalFavorites">0</div>
<div class="stat-label">в избранном</div>
</a>
<a href="/applications" class="stat-card">
<div class="stat-value" id="totalApplications">0</div>
<div class="stat-label">откликов</div>
</a>
</div>
<div class="contact-info">
<div class="contact-item">
<i class="fas fa-envelope"></i>
<span id="profileEmail"></span>
</div>
<div class="contact-item">
<i class="fas fa-phone"></i>
<span id="profilePhone"></span>
</div>
<div class="contact-item">
<i class="fab fa-telegram"></i>
<span id="profileTelegram"></span>
</div>
</div>
</div>
<!-- Основной контент -->
<div class="profile-content">
<div class="content-tabs" id="contentTabs">
<!-- Табы будут заполнены динамически -->
</div>
<!-- Главная вкладка -->
<div class="content-pane active" id="mainPane">
<h2 style="margin-bottom: 20px;">Добро пожаловать!</h2>
<p style="color: #4f7092; margin-bottom: 30px;">Здесь будет ваша активность и статистика</p>
<div class="action-buttons">
<a href="/vacancies" class="btn btn-primary">
<i class="fas fa-search"></i> Найти вакансии
</a>
<a href="/resumes" class="btn btn-outline">
<i class="fas fa-users"></i> Найти сотрудников
</a>
</div>
<div id="applicationsPreview" style="margin-top: 30px;"></div>
</div>
<!-- Вкладка с вакансиями -->
<div class="content-pane" id="vacanciesPane">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 15px;">
<h2>Мои вакансии</h2>
<button class="btn btn-primary" onclick="openVacancyModal()">
<i class="fas fa-plus"></i> Создать вакансию
</button>
</div>
<div class="filter-tabs">
<button class="filter-tab active" data-status="all" onclick="filterVacancies('all')">
Все вакансии
</button>
<button class="filter-tab" data-status="active" onclick="filterVacancies('active')">
Активные
</button>
<button class="filter-tab" data-status="inactive" onclick="filterVacancies('inactive')">
Неактивные
</button>
</div>
<div class="vacancy-stats">
<span><i class="fas fa-check-circle" style="color: #10b981;"></i> Активных: <span id="activeCount">0</span></span>
<span><i class="fas fa-pause-circle" style="color: #f97316;"></i> Неактивных: <span id="inactiveCount">0</span></span>
<span><i class="fas fa-layer-group" style="color: #3b82f6;"></i> Всего: <span id="totalCount">0</span></span>
</div>
<div id="vacanciesList" class="vacancies-grid">
<div class="loading">Загрузка вакансий...</div>
</div>
</div>
<!-- Вкладка с резюме -->
<div class="content-pane" id="resumePane">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 15px;">
<h2>Мое резюме</h2>
<div style="display: flex; gap: 10px;">
<a href="#" id="previewResumeLink" class="btn btn-info" target="_blank" style="display: none;">
<i class="fas fa-eye"></i> Предпросмотр
</a>
<button class="btn btn-success" onclick="saveResume()">
<i class="fas fa-save"></i> Сохранить
</button>
</div>
</div>
<div class="resume-section">
<div id="resumePreviewLink" style="display: none; margin-bottom: 20px;"></div>
<div class="resume-field">
<label>Желаемая должность</label>
<input type="text" id="desiredPosition" placeholder="Например: Frontend-разработчик">
</div>
<div class="resume-field">
<label>О себе</label>
<textarea id="aboutMe" rows="4" placeholder="Расскажите о своем опыте"></textarea>
</div>
<div class="resume-field">
<label>Желаемая зарплата</label>
<input type="text" id="desiredSalary" placeholder="от 200 000 ₽">
</div>
<div class="resume-field">
<label>Навыки (теги через запятую)</label>
<input type="text" id="resumeTags" placeholder="Python, JavaScript, React, SQL">
</div>
<h3 style="margin: 30px 0 20px;">Опыт работы</h3>
<div id="experienceContainer"></div>
<button class="btn btn-outline" onclick="addExperience()" style="margin-bottom: 30px;">
<i class="fas fa-plus"></i> Добавить опыт
</button>
<h3 style="margin: 20px 0;">Образование</h3>
<div id="educationContainer"></div>
<button class="btn btn-outline" onclick="addEducation()">
<i class="fas fa-plus"></i> Добавить образование
</button>
</div>
</div>
<!-- Вкладка с компанией -->
<div class="content-pane" id="companyPane">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 15px;">
<h2>Информация о компании</h2>
<button class="btn btn-success" onclick="saveCompany()">
<i class="fas fa-save"></i> Сохранить
</button>
</div>
<div class="resume-section">
<div class="resume-field">
<label>Название компании <span style="color: #b91c1c;">*</span></label>
<input type="text" id="companyName" placeholder="ООО «Технологии»" required>
</div>
<div class="resume-field">
<label>Логотип (URL)</label>
<input type="text" id="companyLogo" placeholder="https://example.com/logo.png">
</div>
<div class="resume-field">
<label>Сайт компании</label>
<input type="url" id="companyWebsite" placeholder="https://example.com">
</div>
<div class="resume-field">
<label>Описание компании</label>
<textarea id="companyDescription" rows="5" placeholder="Расскажите о компании, миссии, ценностях..."></textarea>
</div>
<div class="resume-field">
<label>Адрес</label>
<input type="text" id="companyAddress" placeholder="г. Москва, ул. Примерная, д. 1">
</div>
<div class="resume-field">
<label>Контактный телефон</label>
<input type="tel" id="companyPhone" placeholder="+7 999 123-45-67">
</div>
<div class="resume-field">
<label>Контактный email</label>
<input type="email" id="companyEmail" placeholder="info@company.ru">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Модальное окно создания/редактирования вакансии -->
<div id="vacancyModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); align-items: center; justify-content: center; z-index: 1000;">
<div style="background: white; max-width: 500px; width: 90%; border-radius: 40px; padding: 40px; max-height: 90vh; overflow-y: auto;">
<h3 style="margin-bottom: 20px;" id="vacancyModalTitle">Новая вакансия</h3>
<input type="hidden" id="editVacancyId">
<div class="resume-field">
<label>Название <span style="color: #b91c1c;">*</span></label>
<input type="text" id="vacancyTitle" placeholder="Менеджер по продажам">
</div>
<div class="resume-field">
<label>Зарплата</label>
<input type="text" id="vacancySalary" placeholder="от 100 000 ₽">
</div>
<div class="resume-field">
<label>Теги (через запятую)</label>
<input type="text" id="vacancyTags" placeholder="Python, JavaScript, React">
</div>
<div class="resume-field">
<label>Описание</label>
<textarea id="vacancyDescription" rows="4" placeholder="Обязанности, требования..."></textarea>
</div>
<div class="resume-field">
<label>Контакт</label>
<input type="text" id="vacancyContact" placeholder="@telegram или email">
</div>
<div class="resume-field" id="activeStatusField" style="display: none;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="vacancyActive" checked>
<span>Вакансия активна</span>
</label>
</div>
<div style="display: flex; gap: 10px; margin-top: 30px;">
<button class="btn btn-primary" onclick="saveVacancy()" id="saveVacancyBtn" style="flex: 1;">Создать</button>
<button class="btn btn-outline" onclick="closeVacancyModal()" style="flex: 1;">Отмена</button>
</div>
</div>
</div>
<!-- Модальное окно для QR-кода профиля -->
<div class="qr-modal" id="qrProfileModal">
<div class="qr-modal-content">
<button class="qr-modal-close" onclick="closeProfileQRModal()">&times;</button>
<h2><i class="fas fa-qrcode"></i> <span id="qrProfileName"></span></h2>
<div class="qr-subtitle" id="qrProfileUrl">yarmarka.rabota.today/user/</div>
<div class="qr-container" id="qrProfileContainer">
<canvas id="qrProfileCanvas" width="250" height="250"></canvas>
<div class="qr-logo-overlay" id="qrProfileLogoOverlay" style="display: none;">
<img id="qrProfileLogoImg" src="" alt="avatar" style="display: none;">
<i id="qrProfileLogoIcon" class="fas fa-user"></i>
</div>
</div>
<div class="qr-stats">
<div class="qr-stat-item">
<div class="qr-stat-value" id="qrProfileRole"></div>
<div class="qr-stat-label">роль</div>
</div>
<div class="qr-stat-item">
<div class="qr-stat-value" id="qrProfileSince"></div>
<div class="qr-stat-label">на платформе</div>
</div>
<div class="qr-stat-item">
<div class="qr-stat-value" id="qrProfileActivity"></div>
<div class="qr-stat-label">активность</div>
</div>
</div>
<div class="qr-actions">
<button class="qr-action-btn primary" onclick="downloadProfileQR()">
<i class="fas fa-download"></i> Скачать PNG
</button>
<button class="qr-action-btn secondary" onclick="copyProfileLink()">
<i class="fas fa-link"></i> Копировать ссылку
</button>
<button class="qr-action-btn secondary" onclick="printProfileQR()">
<i class="fas fa-print"></i> Распечатать
</button>
</div>
<div class="qr-share-buttons">
<button class="qr-share-btn whatsapp" onclick="shareProfileQR('whatsapp')">
<i class="fab fa-whatsapp"></i>
</button>
<button class="qr-share-btn telegram" onclick="shareProfileQR('telegram')">
<i class="fab fa-telegram"></i>
</button>
<button class="qr-share-btn email" onclick="shareProfileQR('email')">
<i class="fas fa-envelope"></i>
</button>
</div>
</div>
</div>
<!-- Уведомления -->
<div class="notification" id="notification"></div>
<script>
// ========== КОНФИГУРАЦИЯ ==========
const API_BASE_URL = window.location.protocol + '//' + window.location.host + '/api';
let token = localStorage.getItem('accessToken');
let currentUser = null;
let resumeId = null;
let currentEditVacancyId = null;
let currentOpenMenu = null;
let allVacancies = [];
let currentFilter = 'all';
// ========== ПРОВЕРКА АВТОРИЗАЦИИ ==========
if (!token) {
window.location.href = '/login';
}
// ========== ФУНКЦИИ ДЛЯ QR-КОДА ПРОФИЛЯ ==========
// Открыть модальное окно с QR
function openProfileQRModal() {
console.log('📱 Открытие QR-модального окна профиля');
if (!currentUser) {
console.error('❌ Данные пользователя не загружены');
showNotification('Данные пользователя не загружены', 'error');
return;
}
const modal = document.getElementById('qrProfileModal');
if (!modal) {
console.error('❌ Модальное окно не найдено');
showNotification('Ошибка отображения QR-кода', 'error');
return;
}
// Получаем элементы
const qrName = document.getElementById('qrProfileName');
const qrUrl = document.getElementById('qrProfileUrl');
const qrRole = document.getElementById('qrProfileRole');
const qrSince = document.getElementById('qrProfileSince');
const qrActivity = document.getElementById('qrProfileActivity');
const qrCanvas = document.getElementById('qrProfileCanvas');
if (!qrName || !qrUrl || !qrRole || !qrSince || !qrActivity || !qrCanvas) {
console.error('❌ Элементы модального окна не найдены');
showNotification('Ошибка отображения QR-кода', 'error');
return;
}
// Устанавливаем имя пользователя
qrName.textContent = escapeHtml(currentUser.full_name);
// Формируем URL профиля
const profileUrl = window.location.origin + '/user/' + currentUser.id;
qrUrl.textContent = profileUrl.replace('https://', '').replace('http://', '');
// Роль пользователя
let roleText = '';
if (currentUser.role === 'employee') roleText = '👤 Соискатель';
else if (currentUser.role === 'employer') roleText = '🏢 Работодатель';
else if (currentUser.role === 'admin') roleText = '👑 Админ';
qrRole.textContent = roleText;
// Дата регистрации
if (currentUser.created_at) {
try {
const date = new Date(currentUser.created_at);
const now = new Date();
const months = Math.floor((now - date) / (1000 * 60 * 60 * 24 * 30));
qrSince.textContent = (months > 0 ? months : '< 1') + ' мес';
} catch (e) {
console.error('Ошибка при вычислении даты:', e);
qrSince.textContent = '—';
}
} else {
qrSince.textContent = '—';
}
// Активность
qrActivity.textContent = 'активен';
// Генерируем QR-код
generateProfileQRWithLogo(profileUrl);
modal.classList.add('active');
}
// Закрыть модальное окно
function closeProfileQRModal() {
const modal = document.getElementById('qrProfileModal');
if (modal) {
modal.classList.remove('active');
}
}
// Генерация QR-кода с иконкой пользователя
function generateProfileQRWithLogo(text) {
const canvas = document.getElementById('qrProfileCanvas');
const logoOverlay = document.getElementById('qrProfileLogoOverlay');
const logoIcon = document.getElementById('qrProfileLogoIcon');
if (!canvas) {
console.error('❌ Canvas для QR-кода не найден');
return;
}
const options = {
width: 250,
height: 250,
color: {
dark: '#0b1c34',
light: '#ffffff'
},
errorCorrectionLevel: 'H'
};
QRCode.toCanvas(canvas, text, options, function(error) {
if (error) {
console.error('Error generating QR code:', error);
showNotification('Ошибка генерации QR-кода', 'error');
} else {
console.log('✅ QR code generated successfully');
if (logoOverlay) {
logoOverlay.style.display = 'flex';
}
if (logoIcon) {
logoIcon.style.display = 'block';
logoIcon.className = 'fas fa-user';
}
}
});
}
// Скачать QR-код
function downloadProfileQR() {
const canvas = document.getElementById('qrProfileCanvas');
if (!canvas) {
showNotification('Ошибка: QR-код не найден', 'error');
return;
}
const combinedCanvas = document.createElement('canvas');
combinedCanvas.width = canvas.width;
combinedCanvas.height = canvas.height;
const ctx = combinedCanvas.getContext('2d');
ctx.drawImage(canvas, 0, 0);
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(125, 125, 35, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = '#3b82f6';
ctx.font = '30px "Font Awesome 6 Free"';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('', 125, 125);
const link = document.createElement('a');
const filename = `profile_${currentUser.id}.png`;
link.download = filename;
link.href = combinedCanvas.toDataURL('image/png');
link.click();
showNotification('QR-код скачан', 'success');
}
// Копировать ссылку на профиль
function copyProfileLink() {
const url = window.location.origin + '/user/' + currentUser.id;
navigator.clipboard.writeText(url).then(() => {
showNotification('Ссылка скопирована', 'success');
}).catch(() => {
showNotification('Ошибка копирования', 'error');
});
}
// Распечатать QR
function printProfileQR() {
const canvas = document.getElementById('qrProfileCanvas');
if (!canvas) {
showNotification('Ошибка: QR-код не найден', 'error');
return;
}
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<html>
<head>
<title>QR-код профиля</title>
<style>
body { display: flex; justify-content: center; align-items: center; height: 100vh; flex-direction: column; font-family: Arial; margin: 0; padding: 20px; }
h2 { color: #0b1c34; text-align: center; word-break: break-word; }
img { max-width: 300px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); border-radius: 20px; }
.url { color: #4f7092; margin-top: 20px; word-break: break-all; text-align: center; }
.role { margin-bottom: 20px; color: #3b82f6; }
</style>
</head>
<body>
<h2>${escapeHtml(currentUser.full_name)}</h2>
<div class="role">${currentUser.role === 'employee' ? '👤 Соискатель' :
currentUser.role === 'employer' ? '🏢 Работодатель' :
'👑 Администратор'}</div>
<img src="${canvas.toDataURL()}" />
<div class="url">${window.location.origin}/user/${currentUser.id}</div>
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
printWindow.print();
}
// Поделиться QR в соцсетях
function shareProfileQR(platform) {
const url = window.location.origin + '/user/' + currentUser.id;
const text = `Мой профиль на Rabota.Today`;
let shareUrl = '';
switch(platform) {
case 'whatsapp':
shareUrl = `https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`;
break;
case 'telegram':
shareUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`;
break;
case 'email':
shareUrl = `mailto:?subject=${encodeURIComponent('Мой профиль на Rabota.Today')}&body=${encodeURIComponent(text + '\n\n' + url)}`;
break;
}
if (shareUrl) {
window.open(shareUrl, '_blank');
}
}
// ========== СОЗДАНИЕ ОВЕРЛЕЯ ==========
function createMenuOverlay() {
if (!document.getElementById('menuOverlay')) {
const overlay = document.createElement('div');
overlay.id = 'menuOverlay';
overlay.className = 'menu-overlay';
overlay.onclick = closeAllMenus;
document.body.appendChild(overlay);
}
}
// ========== ФУНКЦИИ МЕНЮ ==========
function toggleMenu(vacancyId, event) {
if (!vacancyId) return;
if (event) {
event.stopPropagation();
event.preventDefault();
}
const menu = document.getElementById(`menu-${vacancyId}`);
if (!menu) return;
const overlay = document.getElementById('menuOverlay');
if (currentOpenMenu && currentOpenMenu !== menu) {
currentOpenMenu.classList.remove('active');
}
menu.classList.toggle('active');
if (menu.classList.contains('active')) {
currentOpenMenu = menu;
if (overlay) overlay.classList.add('active');
} else {
currentOpenMenu = null;
if (overlay) overlay.classList.remove('active');
}
}
function closeAllMenus() {
document.querySelectorAll('.menu-dropdown').forEach(menu => {
menu.classList.remove('active');
});
const overlay = document.getElementById('menuOverlay');
if (overlay) overlay.classList.remove('active');
currentOpenMenu = null;
}
function viewVacancy(vacancyId, event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
if (!vacancyId) return;
window.open(`/vacancy/${vacancyId}`, '_blank');
closeAllMenus();
}
async function toggleVacancyStatus(vacancyId, activate, event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
if (!vacancyId) return;
try {
const response = await fetch(`${API_BASE_URL}/vacancies/${vacancyId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ is_active: activate })
});
if (!response.ok) throw new Error('Ошибка обновления статуса');
closeAllMenus();
await loadAllVacancies();
showNotification(`Вакансия ${activate ? 'активирована' : 'деактивирована'}`, 'success');
} catch (error) {
showNotification(error.message, 'error');
}
}
async function editVacancy(vacancyId, event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
if (!vacancyId) return;
closeAllMenus();
try {
const response = await fetch(`${API_BASE_URL}/vacancies/${vacancyId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) throw new Error('Ошибка загрузки');
const vacancy = await response.json();
currentEditVacancyId = vacancyId;
document.getElementById('editVacancyId').value = vacancyId;
document.getElementById('vacancyModalTitle').textContent = 'Редактирование вакансии';
document.getElementById('saveVacancyBtn').textContent = 'Сохранить';
document.getElementById('activeStatusField').style.display = 'block';
document.getElementById('vacancyTitle').value = vacancy.title || '';
document.getElementById('vacancySalary').value = vacancy.salary || '';
document.getElementById('vacancyDescription').value = vacancy.description || '';
document.getElementById('vacancyContact').value = vacancy.contact || '';
document.getElementById('vacancyActive').checked = vacancy.is_active !== false;
if (vacancy.tags && vacancy.tags.length > 0) {
document.getElementById('vacancyTags').value = vacancy.tags.map(t => t.name).join(', ');
} else {
document.getElementById('vacancyTags').value = '';
}
document.getElementById('vacancyModal').style.display = 'flex';
} catch (error) {
showNotification(error.message, 'error');
}
}
async function deleteVacancy(vacancyId, event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
if (!vacancyId) return;
closeAllMenus();
if (!confirm('Вы уверены, что хотите удалить вакансию? Это действие нельзя отменить.')) return;
try {
const response = await fetch(`${API_BASE_URL}/vacancies/${vacancyId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
await loadAllVacancies();
showNotification('Вакансия удалена', 'success');
} else {
throw new Error('Ошибка при удалении');
}
} catch (error) {
showNotification(error.message, 'error');
}
}
// ========== ИНИЦИАЛИЗАЦИЯ МЕНЮ ==========
function initializeMenuHandlers() {
createMenuOverlay();
document.addEventListener('click', function(event) {
if (event.target.closest('.vacancy-menu')) return;
closeAllMenus();
});
}
// ========== УВЕДОМЛЕНИЯ ==========
function showNotification(message, type = 'info') {
const notification = document.getElementById('notification');
if (!notification) return;
let backgroundColor = '#3b82f6';
if (type === 'success') backgroundColor = '#10b981';
if (type === 'error') backgroundColor = '#ef4444';
notification.style.backgroundColor = backgroundColor;
notification.style.color = 'white';
notification.textContent = message;
notification.style.display = 'block';
setTimeout(() => {
notification.style.display = 'none';
}, 3000);
}
// ========== ЗАГРУЗКА ПРОФИЛЯ ==========
async function loadProfile() {
try {
const response = await fetch(`${API_BASE_URL}/user`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
if (response.status === 401) {
localStorage.removeItem('accessToken');
window.location.href = '/login';
return;
}
throw new Error('Ошибка загрузки');
}
currentUser = await response.json();
console.log('✅ Данные пользователя загружены:', currentUser);
document.getElementById('profileName').textContent = currentUser.full_name || 'Не указано';
document.getElementById('profileEmail').textContent = currentUser.email || '—';
document.getElementById('profilePhone').textContent = currentUser.phone || '—';
document.getElementById('profileTelegram').textContent = currentUser.telegram || '—';
let roleText = '';
if (currentUser.role === 'employee') roleText = 'Соискатель';
else if (currentUser.role === 'employer') roleText = 'Работодатель';
else if (currentUser.role === 'admin') roleText = 'Администратор';
document.getElementById('profileRole').textContent = roleText;
await loadUserStats();
setupTabs();
updateNavigation();
} catch (error) {
console.error('Error loading profile:', error);
document.querySelector('.profile-sidebar').innerHTML = `
<div class="error-message">
<i class="fas fa-exclamation-circle"></i>
Ошибка загрузки профиля
</div>
`;
}
}
// ========== ЗАГРУЗКА СТАТИСТИКИ ==========
async function loadUserStats() {
try {
const favResponse = await fetch(`${API_BASE_URL}/favorites`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (favResponse.ok) {
const favorites = await favResponse.json();
document.getElementById('totalFavorites').textContent = favorites.length || 0;
}
const appsResponse = await fetch(`${API_BASE_URL}/applications/stats`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (appsResponse.ok) {
const appStats = await appsResponse.json();
document.getElementById('totalApplications').textContent = appStats.total || 0;
if (currentUser && currentUser.role === 'employee') {
showApplicationsPreview(appStats);
}
}
} catch (error) {
console.error('Error loading stats:', error);
}
}
function showApplicationsPreview(stats) {
const container = document.getElementById('applicationsPreview');
if (!container) return;
container.innerHTML = `
<h3 style="margin-bottom: 15px;">Мои отклики</h3>
<div class="applications-stats">
<div class="stat-item pending">
<div class="number">${stats.pending || 0}</div>
<div class="label">Ожидают</div>
</div>
<div class="stat-item viewed">
<div class="number">${stats.viewed || 0}</div>
<div class="label">Просмотрены</div>
</div>
<div class="stat-item accepted">
<div class="number">${stats.accepted || 0}</div>
<div class="label">Приняты</div>
</div>
<div class="stat-item rejected">
<div class="number">${stats.rejected || 0}</div>
<div class="label">Отклонены</div>
</div>
</div>
<a href="/applications" class="btn btn-outline" style="width: 100%; justify-content: center;">
<i class="fas fa-arrow-right"></i> Перейти к откликам
</a>
`;
}
// ========== НАСТРОЙКА ТАБОВ ==========
function setupTabs() {
const tabsContainer = document.getElementById('contentTabs');
if (!tabsContainer) return;
if (currentUser.role === 'employer') {
tabsContainer.innerHTML = `
<div class="content-tab active" onclick="switchTab('main')">Главная</div>
<div class="content-tab" onclick="switchTab('vacancies')">Вакансии</div>
<div class="content-tab" onclick="switchTab('company')">Компания</div>
`;
loadAllVacancies();
loadCompany();
} else if (currentUser.role === 'employee') {
tabsContainer.innerHTML = `
<div class="content-tab active" onclick="switchTab('main')">Главная</div>
<div class="content-tab" onclick="switchTab('resume')">Резюме</div>
`;
loadResume();
}
}
function switchTab(tab) {
document.querySelectorAll('.content-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.content-pane').forEach(p => p.classList.remove('active'));
if (tab === 'main') {
document.querySelectorAll('.content-tab')[0]?.classList.add('active');
document.getElementById('mainPane')?.classList.add('active');
} else if (tab === 'vacancies') {
document.querySelectorAll('.content-tab')[1]?.classList.add('active');
document.getElementById('vacanciesPane')?.classList.add('active');
} else if (tab === 'resume') {
if (currentUser.role === 'employee') {
document.querySelectorAll('.content-tab')[1]?.classList.add('active');
} else {
document.querySelectorAll('.content-tab')[2]?.classList.add('active');
}
document.getElementById('resumePane')?.classList.add('active');
} else if (tab === 'company') {
document.querySelectorAll('.content-tab')[2]?.classList.add('active');
document.getElementById('companyPane')?.classList.add('active');
}
}
// ========== ОБНОВЛЕНИЕ НАВИГАЦИИ ==========
function updateNavigation() {
const nav = document.getElementById('nav');
if (!nav) return;
if (!currentUser) {
nav.innerHTML = `<a href="/login">Войти</a><a href="/register">Регистрация</a>`;
return;
}
let adminBadge = currentUser.is_admin ? '<span class="admin-badge">Admin</span>' : '';
let firstName = currentUser.full_name ? currentUser.full_name.split(' ')[0] : 'Профиль';
nav.innerHTML = `
<a href="/">Главная</a>
<a href="/vacancies">Вакансии</a>
<a href="/resumes">Резюме</a>
<a href="/favorites">Избранное</a>
<a href="/applications">Отклики</a>
<a href="/profile" class="profile-link">
<i class="fas fa-user-circle"></i>
<span class="user-name">${escapeHtml(firstName)}</span>
${adminBadge}
</a>
<a href="#" onclick="logout()" style="color: #ff6b6b;">Выйти</a>
`;
}
// ========== РАБОТА С РЕЗЮМЕ ==========
function addExperience(position = '', company = '', period = '', description = '') {
const container = document.getElementById('experienceContainer');
if (!container) return;
const item = document.createElement('div');
item.className = 'exp-item';
item.innerHTML = `
<div class="resume-field">
<label>Должность</label>
<input type="text" class="exp-position" value="${escapeHtml(position)}" placeholder="Например: Senior Frontend Developer">
</div>
<div class="resume-field">
<label>Компания</label>
<input type="text" class="exp-company" value="${escapeHtml(company)}" placeholder="Название компании">
</div>
<div class="resume-field">
<label>Период работы</label>
<input type="text" class="exp-period" value="${escapeHtml(period)}" placeholder="2022-2024">
</div>
<div class="resume-field">
<label>Обязанности и достижения</label>
<textarea class="exp-description" rows="3" placeholder="Опишите ваши обязанности, проекты и достижения...">${escapeHtml(description)}</textarea>
</div>
<div class="item-actions">
<button class="btn btn-outline" onclick="this.closest('.exp-item').remove()">
<i class="fas fa-trash"></i> Удалить
</button>
</div>
`;
container.appendChild(item);
}
function addEducation(institution = '', specialty = '', year = '') {
const container = document.getElementById('educationContainer');
if (!container) return;
const item = document.createElement('div');
item.className = 'edu-item';
item.innerHTML = `
<div class="resume-field">
<label>Учебное заведение</label>
<input type="text" class="edu-institution" value="${escapeHtml(institution)}">
</div>
<div class="resume-field">
<label>Специальность</label>
<input type="text" class="edu-specialty" value="${escapeHtml(specialty)}">
</div>
<div class="resume-field">
<label>Год окончания</label>
<input type="text" class="edu-year" value="${escapeHtml(year)}">
</div>
<div class="item-actions">
<button class="btn btn-outline" onclick="this.closest('.edu-item').remove()">
<i class="fas fa-trash"></i> Удалить
</button>
</div>
`;
container.appendChild(item);
}
async function loadResume() {
try {
const response = await fetch(`${API_BASE_URL}/resume`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.status === 404) {
addExperience();
addEducation();
return;
}
if (!response.ok) throw new Error('Ошибка загрузки');
const resume = await response.json();
resumeId = resume.id;
document.getElementById('desiredPosition').value = resume.desired_position || '';
document.getElementById('aboutMe').value = resume.about_me || '';
document.getElementById('desiredSalary').value = resume.desired_salary || '';
if (resume.tags && resume.tags.length > 0) {
document.getElementById('resumeTags').value = resume.tags.map(t => t.name).join(', ');
}
const previewLink = document.getElementById('previewResumeLink');
if (previewLink) {
previewLink.href = `/resume/${resumeId}`;
previewLink.style.display = 'inline-flex';
}
const resumePreviewLink = document.getElementById('resumePreviewLink');
if (resumePreviewLink) {
resumePreviewLink.innerHTML = `
<a href="/resume/${resumeId}" target="_blank" class="resume-preview-link">
<i class="fas fa-external-link-alt"></i> Посмотреть, как видят ваше резюме работодатели
</a>
`;
resumePreviewLink.style.display = 'block';
}
document.getElementById('experienceContainer').innerHTML = '';
resume.work_experience.forEach(exp => {
addExperience(
exp.position,
exp.company,
exp.period,
exp.description || ''
);
});
document.getElementById('educationContainer').innerHTML = '';
resume.education.forEach(edu => {
addEducation(edu.institution, edu.specialty, edu.graduation_year);
});
} catch (error) {
console.error('Error loading resume:', error);
}
}
async function saveResume() {
const workExperience = [];
document.querySelectorAll('#experienceContainer .exp-item').forEach(item => {
workExperience.push({
position: item.querySelector('.exp-position')?.value || '',
company: item.querySelector('.exp-company')?.value || '',
period: item.querySelector('.exp-period')?.value || null,
description: item.querySelector('.exp-description')?.value || null
});
});
const education = [];
document.querySelectorAll('#educationContainer .edu-item').forEach(item => {
education.push({
institution: item.querySelector('.edu-institution')?.value || '',
specialty: item.querySelector('.edu-specialty')?.value || null,
graduation_year: item.querySelector('.edu-year')?.value || null
});
});
const tagsInput = document.getElementById('resumeTags')?.value || '';
const tags = tagsInput
.split(',')
.map(t => t.trim())
.filter(t => t.length > 0);
const resumeData = {
desired_position: document.getElementById('desiredPosition').value || null,
about_me: document.getElementById('aboutMe').value || null,
desired_salary: document.getElementById('desiredSalary').value || null,
work_experience: workExperience,
education: education,
tags: tags
};
try {
const response = await fetch(`${API_BASE_URL}/resume`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(resumeData)
});
if (!response.ok) throw new Error('Ошибка сохранения');
const saved = await response.json();
resumeId = saved.id;
const previewLink = document.getElementById('previewResumeLink');
if (previewLink) {
previewLink.href = `/resume/${resumeId}`;
previewLink.style.display = 'inline-flex';
}
const resumePreviewLink = document.getElementById('resumePreviewLink');
if (resumePreviewLink) {
resumePreviewLink.innerHTML = `
<a href="/resume/${resumeId}" target="_blank" class="resume-preview-link">
<i class="fas fa-external-link-alt"></i> Посмотреть, как видят ваше резюме работодатели
</a>
`;
resumePreviewLink.style.display = 'block';
}
showNotification('Резюме сохранено!', 'success');
} catch (error) {
showNotification(error.message, 'error');
}
}
// ========== РАБОТА С ВАКАНСИЯМИ ==========
async function loadAllVacancies() {
try {
console.log('📥 Загрузка всех вакансий пользователя...');
const response = await fetch(`${API_BASE_URL}/vacancies/my-all`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
console.warn('⚠️ Новый эндпоинт не работает, пробуем старый...');
const fallbackResponse = await fetch(`${API_BASE_URL}/vacancies`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!fallbackResponse.ok) throw new Error('Ошибка загрузки');
allVacancies = await fallbackResponse.json();
} else {
allVacancies = await response.json();
}
const activeCount = allVacancies.filter(v => v.is_active).length;
const inactiveCount = allVacancies.filter(v => !v.is_active).length;
console.log(`✅ Загружено вакансий: ${allVacancies.length}`);
console.log(` 📊 Активные: ${activeCount}`);
console.log(` 📊 Неактивные: ${inactiveCount}`);
updateVacancyCounts();
filterVacancies(currentFilter);
} catch (error) {
console.error('❌ Ошибка загрузки вакансий:', error);
document.getElementById('vacanciesList').innerHTML = '<div class="loading">Ошибка загрузки вакансий</div>';
}
}
function updateVacancyCounts() {
const activeCount = allVacancies.filter(v => v.is_active).length;
const inactiveCount = allVacancies.filter(v => !v.is_active).length;
document.getElementById('activeCount').textContent = activeCount;
document.getElementById('inactiveCount').textContent = inactiveCount;
document.getElementById('totalCount').textContent = allVacancies.length;
}
function filterVacancies(filter) {
currentFilter = filter;
document.querySelectorAll('.filter-tab').forEach(tab => {
tab.classList.remove('active');
if (tab.dataset.status === filter) {
tab.classList.add('active');
}
});
let filteredVacancies = allVacancies;
if (filter === 'active') {
filteredVacancies = allVacancies.filter(v => v.is_active === true);
} else if (filter === 'inactive') {
filteredVacancies = allVacancies.filter(v => v.is_active === false);
}
displayVacancies(filteredVacancies);
}
function displayVacancies(vacancies) {
const container = document.getElementById('vacanciesList');
if (!container) return;
if (vacancies.length === 0) {
let message = '📭 Нет вакансий';
if (currentFilter === 'active') message = '📭 Нет активных вакансий';
if (currentFilter === 'inactive') message = '📭 Нет неактивных вакансий';
container.innerHTML = `<div class="loading">${message}</div>`;
return;
}
container.innerHTML = vacancies.map(v => `
<div class="vacancy-card" id="vacancy-${v.id}">
<div class="vacancy-menu">
<div class="menu-trigger" onclick="toggleMenu(${v.id}, event)">
<i class="fas fa-ellipsis-v"></i>
</div>
<div class="menu-dropdown" id="menu-${v.id}">
<div class="menu-item view" onclick="viewVacancy(${v.id}, event)">
<i class="fas fa-eye"></i>
<span>Просмотреть</span>
</div>
<div class="menu-item edit" onclick="editVacancy(${v.id}, event)">
<i class="fas fa-edit"></i>
<span>Редактировать</span>
</div>
${v.is_active ?
`<div class="menu-item deactivate" onclick="toggleVacancyStatus(${v.id}, false, event)">
<i class="fas fa-pause-circle"></i>
<span>Деактивировать</span>
</div>` :
`<div class="menu-item activate" onclick="toggleVacancyStatus(${v.id}, true, event)">
<i class="fas fa-play-circle"></i>
<span>Активировать</span>
</div>`
}
<div class="menu-divider"></div>
<div class="menu-item delete" onclick="deleteVacancy(${v.id}, event)">
<i class="fas fa-trash-alt"></i>
<span>Удалить</span>
</div>
</div>
</div>
<h3>${escapeHtml(v.title)}</h3>
<div style="color: #3b82f6; font-size: 14px; margin-bottom: 5px; display: flex; align-items: center; gap: 8px;">
<i class="fas fa-building"></i> ${escapeHtml(v.company_name || 'Компания')}
<span class="status-badge ${v.is_active ? 'active' : 'inactive'}">
${v.is_active ? '✅ Активна' : '⏸️ Неактивна'}
</span>
</div>
<div class="vacancy-salary">${escapeHtml(v.salary || 'з/п не указана')}</div>
<div class="vacancy-tags">
${(v.tags || []).map(t => `<span class="tag">${escapeHtml(t.name)}</span>`).join('')}
</div>
<p style="color: #4f7092; margin: 10px 0;">${escapeHtml((v.description || '').substring(0, 100))}...</p>
<div class="vacancy-footer">
<span><i class="fab fa-telegram"></i> ${escapeHtml(v.contact || '—')}</span>
<span><i class="fas fa-eye"></i> ${v.views || 0}</span>
<span><i class="fas fa-calendar"></i> ${new Date(v.created_at).toLocaleDateString()}</span>
</div>
</div>
`).join('');
}
// ========== РАБОТА С КОМПАНИЕЙ ==========
async function loadCompany() {
try {
const response = await fetch(`${API_BASE_URL}/company`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.status === 404) {
return;
}
if (!response.ok) throw new Error('Ошибка загрузки');
const company = await response.json();
document.getElementById('companyName').value = company.name || '';
document.getElementById('companyLogo').value = company.logo || '';
document.getElementById('companyWebsite').value = company.website || '';
document.getElementById('companyDescription').value = company.description || '';
document.getElementById('companyAddress').value = company.address || '';
document.getElementById('companyPhone').value = company.phone || '';
document.getElementById('companyEmail').value = company.email || '';
} catch (error) {
console.error('Error loading company:', error);
}
}
async function saveCompany() {
const companyData = {
name: document.getElementById('companyName').value,
logo: document.getElementById('companyLogo').value || null,
website: document.getElementById('companyWebsite').value || null,
description: document.getElementById('companyDescription').value || null,
address: document.getElementById('companyAddress').value || null,
phone: document.getElementById('companyPhone').value || null,
email: document.getElementById('companyEmail').value || null
};
if (!companyData.name) {
showNotification('Введите название компании', 'error');
return;
}
try {
const response = await fetch(`${API_BASE_URL}/company`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(companyData)
});
if (!response.ok) throw new Error('Ошибка сохранения');
showNotification('Информация о компании сохранена!', 'success');
} catch (error) {
showNotification(error.message, 'error');
}
}
// ========== РАБОТА С МОДАЛЬНЫМ ОКНОМ ВАКАНСИИ ==========
function openVacancyModal() {
currentEditVacancyId = null;
document.getElementById('vacancyModalTitle').textContent = 'Новая вакансия';
document.getElementById('saveVacancyBtn').textContent = 'Создать';
document.getElementById('activeStatusField').style.display = 'none';
document.getElementById('editVacancyId').value = '';
document.getElementById('vacancyTitle').value = '';
document.getElementById('vacancySalary').value = '';
document.getElementById('vacancyTags').value = '';
document.getElementById('vacancyDescription').value = '';
document.getElementById('vacancyContact').value = '';
document.getElementById('vacancyActive').checked = true;
document.getElementById('vacancyModal').style.display = 'flex';
}
function closeVacancyModal() {
document.getElementById('vacancyModal').style.display = 'none';
currentEditVacancyId = null;
}
async function saveVacancy() {
const tagsInput = document.getElementById('vacancyTags')?.value || '';
const tags = tagsInput
.split(',')
.map(t => t.trim())
.filter(t => t.length > 0);
const vacancyData = {
title: document.getElementById('vacancyTitle').value,
salary: document.getElementById('vacancySalary').value || null,
description: document.getElementById('vacancyDescription').value || null,
contact: document.getElementById('vacancyContact').value || null,
tags: tags
};
if (!vacancyData.title) {
showNotification('Введите название вакансии', 'error');
return;
}
if (currentEditVacancyId) {
vacancyData.is_active = document.getElementById('vacancyActive').checked;
}
try {
let response;
let url = `${API_BASE_URL}/vacancies`;
if (currentEditVacancyId) {
response = await fetch(`${url}/${currentEditVacancyId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(vacancyData)
});
} else {
response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(vacancyData)
});
}
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Ошибка сохранения');
}
closeVacancyModal();
await loadAllVacancies();
showNotification(currentEditVacancyId ? 'Вакансия обновлена!' : 'Вакансия создана!', 'success');
} catch (error) {
showNotification(error.message, 'error');
}
}
// ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==========
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function logout() {
localStorage.removeItem('accessToken');
window.location.href = '/';
}
// ========== ИНИЦИАЛИЗАЦИЯ ==========
document.addEventListener('DOMContentLoaded', function() {
createMenuOverlay();
initializeMenuHandlers();
loadProfile();
});
// Закрытие модалки по клику вне окна
window.onclick = function(event) {
const vacancyModal = document.getElementById('vacancyModal');
if (event.target === vacancyModal) {
vacancyModal.style.display = 'none';
}
const qrModal = document.getElementById('qrProfileModal');
if (event.target === qrModal) {
qrModal.classList.remove('active');
}
};
</script>
</body>
</html>