Files
hh_auto/templates/index.html
T

708 lines
30 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">
<title>hh.ru автоотклик</title>
<style>
:root {
--bg: #0f1117; --card: #1a1d27; --border: #2a2f3a; --text: #e5e7eb;
--muted: #9ca3af; --accent: #3b82f6; --accent-hover: #60a5fa;
--green: #22c55e; --red: #ef4444; --amber: #f59e0b;
--input-bg: #12141c; --hover: #1e222d;
--secondary: #2a2f3a; --secondary-hover: #333a47;
--danger-bg: #3a1d1d; --danger-hover: #4a2525;
--done-bg: #14351f; --new-bg: #1a2f4a; --err-bg: #3a1d1d;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--bg); color: var(--text); font-size: 14px; }
header { background: var(--card); border-bottom: 1px solid var(--border);
padding: 12px 24px; display: flex; align-items: center; gap: 24px; }
header h1 { font-size: 18px; font-weight: 600; }
nav { display: flex; gap: 4px; }
nav button { border: none; background: none; padding: 8px 14px; cursor: pointer;
border-radius: 8px; font-size: 14px; color: var(--muted); }
nav button.active { background: var(--accent); color: #fff; }
main { max-width: 1100px; margin: 24px auto; padding: 0 24px; }
.card { background: var(--card); border: 1px solid var(--border);
border-radius: 12px; padding: 20px; margin-bottom: 16px; }
.card h2 { font-size: 15px; margin-bottom: 14px; }
.row { display: flex; gap: 12px; flex-wrap: wrap; }
.field { margin-bottom: 12px; flex: 1; min-width: 200px; }
.field label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 4px; }
input[type=text], input[type=number], select, textarea {
width: 100%; padding: 8px 10px; border: 1px solid var(--border);
border-radius: 8px; font-size: 14px; font-family: inherit;
background: var(--input-bg); color: var(--text); }
input[type=file] { color: var(--muted); font-size: 13px; }
input[type=file]::file-selector-button {
background: var(--secondary); color: var(--text); border: none;
border-radius: 6px; padding: 6px 12px; margin-right: 10px; cursor: pointer; }
input[type=file]::file-selector-button:hover { background: var(--secondary-hover); }
textarea { resize: vertical; }
button { padding: 8px 16px; border: none; border-radius: 8px; cursor: pointer;
font-size: 14px; background: var(--accent); color: #fff; }
button:hover { background: var(--accent-hover); }
button.secondary { background: var(--secondary); color: var(--text); }
button.secondary:hover { background: var(--secondary-hover); }
button.danger { background: var(--danger-bg); color: var(--red); }
button.danger:hover { background: var(--danger-hover); }
button:disabled { opacity: .5; cursor: not-allowed; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--border);
vertical-align: top; }
th { font-size: 12px; color: var(--muted); font-weight: 500; }
tr:hover td { background: var(--hover); }
.badge { display: inline-block; padding: 2px 8px; border-radius: 999px;
font-size: 12px; }
.badge.done { background: var(--done-bg); color: var(--green); }
.badge.new { background: var(--new-bg); color: var(--accent); }
.badge.err { background: var(--err-bg); color: var(--red); }
.muted { color: var(--muted); font-size: 12px; }
.error { color: var(--red); font-size: 13px; margin-top: 6px; }
.ok { color: var(--green); font-size: 13px; margin-top: 6px; }
.account-item { display: flex; justify-content: space-between; align-items: center;
padding: 10px 12px; border: 1px solid var(--border);
border-radius: 8px; margin-bottom: 8px; background: var(--card); }
.account-item .name { font-weight: 600; }
.account-item .actions { display: flex; gap: 8px; }
.hidden { display: none !important; }
.texts-panel textarea { min-height: 90px; margin-bottom: 8px; }
.texts-panel .item { margin-bottom: 14px; }
.texts-panel .item .title { font-weight: 600; margin-bottom: 4px; }
.toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
.toolbar .spacer { flex: 1; }
.status-line { font-size: 13px; margin-top: 8px; }
.empty { color: var(--muted); text-align: center; padding: 30px 0; }
.link { color: var(--accent); text-decoration: none; }
.link:hover { text-decoration: underline; }
.actions-cell { display: flex; gap: 6px; flex-wrap: wrap; }
.actions-cell button { padding: 4px 10px; font-size: 12px; }
.modal { position: fixed; inset: 0; background: rgba(0,0,0,.75); display: flex;
align-items: center; justify-content: center; z-index: 100; }
.modal-box { background: var(--card); border: 1px solid var(--border);
border-radius: 12px; padding: 20px; max-width: 920px; width: 94%;
max-height: 92vh; overflow: auto; }
.modal-box img { max-width: 100%; border: 1px solid var(--border);
border-radius: 8px; display: block; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 5px; }
::-webkit-scrollbar-thumb:hover { background: var(--secondary-hover); }
</style>
</head>
<body>
<header>
<h1>hh.ru автоотклик</h1>
<nav>
<button id="tab-btn-accounts" class="active" onclick="showTab('accounts')">Аккаунты</button>
<button id="tab-btn-vacancies" onclick="showTab('vacancies')">Вакансии</button>
</nav>
<div style="margin-left:auto; display:flex; align-items:center; gap:12px">
<span class="muted" id="current-user"></span>
<button class="secondary" onclick="logout()">Выйти</button>
</div>
</header>
<main>
<!-- ============ ВКЛАДКА: АККАУНТЫ ============ -->
<section id="tab-accounts">
<div class="card">
<h2>Новый аккаунт</h2>
<div class="row">
<div class="field" style="flex:0 0 220px">
<label>Имя аккаунта</label>
<input type="text" id="new-name" placeholder="например, my_account">
</div>
<div class="field" style="flex:0 0 160px">
<label>Регион</label>
<select id="new-area">
<option value="1">Москва</option>
<option value="2">Санкт-Петербург</option>
<option value="113">Россия</option>
<option value="3">Екатеринбург</option>
<option value="4">Новосибирск</option>
<option value="88">Казань</option>
</select>
</div>
</div>
<div class="field">
<label>Поисковые запросы (по одному на строку, можно с операторами: NAME:Python, NOT senior)</label>
<textarea id="new-queries" rows="3" placeholder="Python разработчик&#10;NAME:Python backend&#10;NAME:Python AND NOT NAME:senior"></textarea>
</div>
<div class="field">
<label>Стиль отклика (необязательно)</label>
<input type="text" id="new-style" placeholder="Кратко, по делу, без канцелярита">
</div>
<div class="field">
<label>Резюме (текст, который ИИ использует для откликов)</label>
<textarea id="new-resume" rows="5" placeholder="Имя, опыт, навыки, проекты..."></textarea>
<div style="margin-top:6px; display:flex; gap:8px; align-items:center">
<input type="file" id="new-resume-file" accept=".txt,.md,.docx,.pdf" style="flex:1">
<button class="secondary" onclick="uploadResume('new')">Загрузить файл</button>
</div>
</div>
<button onclick="createAccount()">Создать аккаунт</button>
<div id="new-msg"></div>
</div>
<div class="card">
<h2>Аккаунты</h2>
<div id="accounts-list"><div class="empty">Загрузка...</div></div>
</div>
</section>
<!-- ============ ВКЛАДКА: ВАКАНСИИ ============ -->
<section id="tab-vacancies" class="hidden">
<div class="card">
<div class="toolbar">
<select id="acc-select" style="max-width:260px" onchange="selectAccount()"></select>
<button class="secondary" onclick="loadAccountSettings()">Обновить</button>
<div class="spacer"></div>
<button class="secondary" onclick="openLogin()">Войти в hh.ru</button>
<button onclick="runSearch()">Найти вакансии</button>
</div>
<div id="acc-info" class="muted"></div>
</div>
<div class="card">
<h2>Настройки аккаунта</h2>
<div class="row">
<div class="field" style="flex:0 0 160px">
<label>Регион</label>
<select id="cfg-area">
<option value="1">Москва</option>
<option value="2">Санкт-Петербург</option>
<option value="113">Россия</option>
<option value="3">Екатеринбург</option>
<option value="4">Новосибирск</option>
<option value="88">Казань</option>
</select>
</div>
<div class="field" style="flex:0 0 120px">
<label>Страниц</label>
<input type="number" id="cfg-pages" min="1" max="10" value="1">
</div>
</div>
<div class="field">
<label>Поисковые запросы</label>
<textarea id="cfg-queries" rows="3"></textarea>
</div>
<div class="field">
<label>Стиль отклика</label>
<input type="text" id="cfg-style">
</div>
<div class="field">
<label>Резюме</label>
<textarea id="cfg-resume" rows="6"></textarea>
<div style="margin-top:6px; display:flex; gap:8px; align-items:center">
<input type="file" id="cfg-resume-file" accept=".txt,.md,.docx,.pdf" style="flex:1">
<button class="secondary" onclick="uploadResume('cfg')">Загрузить файл</button>
</div>
</div>
<div class="field">
<label>Ключ сессии hh.ru (все cookies из браузера)</label>
<input type="password" id="cfg-session" placeholder="hhtoken=...; hhuid=...; hhrole=...; _xsrf=...">
<div style="margin-top:6px; display:flex; gap:8px; align-items:center">
<button class="secondary" onclick="saveSession()">Сохранить ключ</button>
<span id="session-msg" class="muted"></span>
</div>
</div>
<div class="field">
<label>Резюме на hh.ru (для автовыбора в модалке)</label>
<div style="display:flex; gap:8px; align-items:center">
<select id="cfg-resume-id" style="flex:1">
<option value="">— не выбрано —</option>
</select>
<button class="secondary" onclick="loadResumes()">Обновить список</button>
</div>
</div>
<button onclick="saveSettings()">Сохранить настройки</button>
<span id="cfg-msg"></span>
</div>
<div class="card">
<div class="toolbar">
<h2 style="margin:0">Вакансии</h2>
<div class="spacer"></div>
<span id="search-status" class="muted"></span>
<button class="secondary" onclick="resetProgress()">Сбросить прогресс</button>
<button id="btn-generate" onclick="generateTexts()" disabled>Сгенерировать тексты</button>
</div>
<div id="vacancies-table"></div>
</div>
<div class="card texts-panel hidden" id="texts-panel">
<h2>Тексты откликов (можно редактировать)</h2>
<div id="texts-list"></div>
</div>
</section>
</main>
<!-- ============ МОДАЛКА: ПРОВЕРКА ОТКЛИКА ============ -->
<div id="shot-modal" class="modal hidden" onclick="if(event.target===this) closeShot()">
<div class="modal-box">
<div class="toolbar">
<h2 style="margin:0">Проверьте отклик</h2>
<div class="spacer"></div>
<button class="secondary" onclick="closeShot()">Закрыть</button>
</div>
<div id="shot-note" class="muted" style="margin-bottom:8px"></div>
<img id="shot-img" alt="Скриншот вакансии">
<div class="toolbar" style="margin-top:12px">
<button id="btn-send" onclick="confirmSend()">Отправить отклик</button>
<button id="btn-done" class="secondary" onclick="markDone(currentShotId)">Отправил (отметить)</button>
<span id="send-note" class="muted"></span>
</div>
</div>
</div>
<script>
let currentAccount = null;
let vacancies = [];
let generatedTexts = {};
let currentShotId = null;
let sentTexts = new Set();
// ---------- Вкладки ----------
function showTab(name) {
document.getElementById('tab-accounts').classList.toggle('hidden', name !== 'accounts');
document.getElementById('tab-vacancies').classList.toggle('hidden', name !== 'vacancies');
document.getElementById('tab-btn-accounts').classList.toggle('active', name === 'accounts');
document.getElementById('tab-btn-vacancies').classList.toggle('active', name === 'vacancies');
if (name === 'vacancies') loadAccounts();
}
// ---------- Аккаунты ----------
async function api(url, opts = {}) {
const resp = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
if (resp.status === 401) { window.location = '/login'; throw new Error('Требуется вход'); }
const data = await resp.json().catch(() => ({}));
if (!resp.ok) throw new Error(data.error || `HTTP ${resp.status}`);
return data;
}
function logout() { window.location = '/logout'; }
async function loadAccounts() {
try {
const me = await api('/api/me');
const lim = me.limit ? ` · генерации сегодня: ${me.used}/${me.limit}` : '';
document.getElementById('current-user').textContent = me.user + lim;
} catch (e) { /* 401 уже редиректит */ }
const list = await api('/api/accounts');
const sel = document.getElementById('acc-select');
const prev = sel.value;
sel.innerHTML = '';
list.forEach(a => {
const opt = document.createElement('option');
opt.value = a; opt.textContent = a;
sel.appendChild(opt);
});
if (list.includes(prev)) sel.value = prev;
renderAccounts(list);
if (list.length) selectAccount();
}
function renderAccounts(list) {
const box = document.getElementById('accounts-list');
if (!list.length) { box.innerHTML = '<div class="empty">Пока нет аккаунтов — создайте первый выше</div>'; return; }
box.innerHTML = '';
list.forEach(name => {
const div = document.createElement('div');
div.className = 'account-item';
div.innerHTML = `
<span class="name">${name}</span>
<span class="actions">
<button class="secondary" onclick="pickAccount('${name}')">Вакансии</button>
<button class="danger" onclick="deleteAccount('${name}')">Удалить</button>
</span>`;
box.appendChild(div);
});
}
async function createAccount() {
const msg = document.getElementById('new-msg');
try {
await api('/api/accounts', {
method: 'POST',
body: JSON.stringify({
name: document.getElementById('new-name').value,
queries: document.getElementById('new-queries').value,
area: document.getElementById('new-area').value,
style: document.getElementById('new-style').value,
resume: document.getElementById('new-resume').value,
}),
});
msg.className = 'ok'; msg.textContent = 'Аккаунт создан';
document.getElementById('new-name').value = '';
await loadAccounts();
} catch (e) { msg.className = 'error'; msg.textContent = e.message; }
}
async function deleteAccount(name) {
if (!confirm(`Удалить аккаунт «${name}»? Сессия и прогресс будут удалены.`)) return;
await api(`/api/accounts/${name}`, { method: 'DELETE' });
await loadAccounts();
}
function pickAccount(name) {
document.getElementById('acc-select').value = name;
selectAccount();
showTab('vacancies');
}
// ---------- Настройки аккаунта ----------
async function selectAccount() {
currentAccount = document.getElementById('acc-select').value;
if (!currentAccount) return;
await loadAccountSettings();
await runSearch();
}
async function loadAccountSettings() {
currentAccount = document.getElementById('acc-select').value;
if (!currentAccount) return;
const data = await api(`/api/accounts/${currentAccount}`);
const cfg = data.config || {};
document.getElementById('cfg-queries').value = (cfg.queries || []).join('\n');
document.getElementById('cfg-area').value = cfg.area || 1;
document.getElementById('cfg-pages').value = cfg.pages || 1;
document.getElementById('cfg-style').value = cfg.style || '';
document.getElementById('cfg-resume').value = data.resume || '';
document.getElementById('cfg-resume-id').value = cfg.resume_id || '';
document.getElementById('acc-info').textContent =
`Откликнуто: ${data.responded.length}`;
// Статус ключа сессии (сам ключ не показываем — он хранится на сервере)
const sm = document.getElementById('session-msg');
if (data.has_session) {
sm.className = 'ok';
sm.textContent = 'Ключ сохранён на сервере. Меняйте только если истёк.';
} else {
sm.className = 'error';
sm.textContent = 'Ключ не задан — вставьте cookies и нажмите «Сохранить ключ»';
}
}
// ---------- Ключ сессии hh.ru ----------
async function saveSession() {
const msg = document.getElementById('session-msg');
const session = document.getElementById('cfg-session').value.trim();
if (!session) { msg.className = 'error'; msg.textContent = 'Вставьте cookies'; return; }
msg.className = ''; msg.textContent = 'Сохраняю и проверяю...';
try {
const data = await api(`/api/accounts/${currentAccount}/session`, {
method: 'POST',
body: JSON.stringify({ session }),
});
msg.className = 'ok';
msg.textContent = data.warning || 'Сохранено, сессия работает';
if (data.resumes) fillResumeSelect(data.resumes);
} catch (e) {
msg.className = 'error'; msg.textContent = e.message;
}
}
async function loadResumes() {
const msg = document.getElementById('session-msg');
msg.className = ''; msg.textContent = 'Загружаю резюме...';
try {
const data = await api(`/api/accounts/${currentAccount}/resumes`);
fillResumeSelect(data.resumes);
msg.className = 'ok'; msg.textContent = `Найдено резюме: ${data.resumes.length}`;
} catch (e) {
msg.className = 'error'; msg.textContent = e.message;
}
}
function fillResumeSelect(resumes) {
const sel = document.getElementById('cfg-resume-id');
const prev = sel.value;
sel.innerHTML = '<option value="">— не выбрано —</option>' +
resumes.map(r => `<option value="${r.id}">${r.title}</option>`).join('');
if (prev) sel.value = prev;
}
async function saveSettings() {
const msg = document.getElementById('cfg-msg');
try {
await api(`/api/accounts/${currentAccount}`, {
method: 'PUT',
body: JSON.stringify({
queries: document.getElementById('cfg-queries').value,
area: document.getElementById('cfg-area').value,
pages: document.getElementById('cfg-pages').value,
style: document.getElementById('cfg-style').value,
resume: document.getElementById('cfg-resume').value,
resume_id: document.getElementById('cfg-resume-id').value,
}),
});
msg.className = 'ok'; msg.textContent = 'Сохранено';
setTimeout(() => msg.textContent = '', 2000);
} catch (e) { msg.className = 'error'; msg.textContent = e.message; }
}
// ---------- Загрузка резюме файлом ----------
async function uploadResume(which) {
const fileInput = document.getElementById(which === 'cfg' ? 'cfg-resume-file' : 'new-resume-file');
const file = fileInput.files[0];
if (!file) { alert('Выберите файл'); return; }
const target = which === 'cfg' ? currentAccount : document.getElementById('new-name').value.trim();
if (!target) { alert('Сначала укажите имя аккаунта'); return; }
const fd = new FormData();
fd.append('file', file);
const resp = await fetch(`/api/accounts/${target}/resume-upload`, { method: 'POST', body: fd });
const data = await resp.json();
if (!resp.ok) { alert('Ошибка: ' + data.error); return; }
const ta = document.getElementById(which === 'cfg' ? 'cfg-resume' : 'new-resume');
ta.value = data.text;
alert(`Резюме загружено (${data.chars} символов). Проверьте текст и сохраните настройки.`);
}
// ---------- Поиск ----------
async function runSearch() {
if (!currentAccount) return;
const status = document.getElementById('search-status');
status.textContent = 'Ищу...';
try {
const data = await api(`/api/accounts/${currentAccount}/search`, { method: 'POST' });
vacancies = data.vacancies;
status.textContent = `Найдено: ${data.total}`;
renderVacancies();
} catch (e) {
status.textContent = '';
document.getElementById('vacancies-table').innerHTML =
`<div class="error">Ошибка поиска: ${e.message}</div>`;
}
}
function renderVacancies() {
const box = document.getElementById('vacancies-table');
if (!vacancies.length) {
box.innerHTML = '<div class="empty">Вакансий нет. Проверьте запросы или нажмите «Найти вакансии»</div>';
return;
}
let html = `<table><thead><tr>
<th style="width:28px"><input type="checkbox" id="check-all" onchange="toggleAll(this)"></th>
<th>Вакансия</th><th>Компания</th><th>Зарплата</th><th>Опыт</th><th>Статус</th><th>Действия</th>
</tr></thead><tbody>`;
vacancies.forEach(v => {
const salary = v.salary
? (v.salary.from ? `от ${v.salary.from.toLocaleString('ru')}` : '') +
(v.salary.to ? ` до ${v.salary.to.toLocaleString('ru')}` : '')
: '—';
const exp = { 'noExperience': 'без опыта', 'between1And3': '1–3 года',
'between3And6': '3–6 лет', 'moreThan6': '6+ лет' }[v.experience] || '—';
const status = v.responded
? '<span class="badge done">откликнуто</span>'
: '<span class="badge new">новая</span>';
html += `<tr data-id="${v.id}">
<td><input type="checkbox" class="v-check" data-id="${v.id}" onchange="updateGenerateBtn()"></td>
<td><a class="link" href="${v.url}" target="_blank">${v.title}</a></td>
<td>${v.company || '—'}</td>
<td>${salary}</td>
<td>${exp}</td>
<td>${status}</td>
<td class="actions-cell">
<button class="secondary" onclick="openVacancy('${v.id}')">Открыть</button>
<button class="secondary" onclick="markDone('${v.id}')">Отправил</button>
</td>
</tr>`;
});
html += '</tbody></table>';
box.innerHTML = html;
updateGenerateBtn();
}
function selectedIds() {
return [...document.querySelectorAll('.v-check:checked')].map(c => c.dataset.id);
}
function toggleAll(cb) {
document.querySelectorAll('.v-check').forEach(c => c.checked = cb.checked);
updateGenerateBtn();
}
function updateGenerateBtn() {
document.getElementById('btn-generate').disabled = selectedIds().length === 0;
}
// ---------- Генерация текстов ----------
async function generateTexts() {
const ids = selectedIds();
if (!ids.length) return;
const btn = document.getElementById('btn-generate');
btn.disabled = true; btn.textContent = 'Генерирую...';
try {
const data = await api(`/api/accounts/${currentAccount}/generate`, {
method: 'POST', body: JSON.stringify({ ids }),
});
generatedTexts = { ...generatedTexts, ...data.texts };
renderTexts(data.errors || {});
} catch (e) {
alert('Ошибка генерации: ' + e.message);
}
btn.textContent = 'Сгенерировать тексты';
updateGenerateBtn();
}
function renderTexts(errors = {}) {
const panel = document.getElementById('texts-panel');
const list = document.getElementById('texts-list');
panel.classList.remove('hidden');
list.innerHTML = '';
vacancies.filter(v => generatedTexts[v.id]).forEach(v => {
const div = document.createElement('div');
div.className = 'item';
const sent = sentTexts.has(v.id);
div.innerHTML = `
<div class="title">${v.title} — ${v.company || ''}</div>
<textarea data-id="${v.id}">${escapeHtml(generatedTexts[v.id])}</textarea>
<div class="toolbar">
${sent
? '<button class="secondary" disabled>Отправлено ✓</button>'
: `<button class="secondary" onclick="sendVacancy('${v.id}')">Отправить отклик</button>
<button class="secondary" onclick="openVacancy('${v.id}')">Открыть в браузере</button>`}
<span class="muted" id="note-${v.id}"></span>
</div>`;
list.appendChild(div);
});
vacancies.filter(v => errors[v.id]).forEach(v => {
const div = document.createElement('div');
div.className = 'item';
div.innerHTML = `<div class="title">${v.title}</div>
<div class="error">Ошибка ИИ: ${escapeHtml(errors[v.id])}</div>`;
list.appendChild(div);
});
}
function escapeHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/"/g,'&quot;');
}
// ---------- Открытие, проверка и отправка ----------
function showShotModal(id, screenshot, note, sent) {
currentShotId = id;
document.getElementById('shot-img').src = screenshot + '?t=' + Date.now();
document.getElementById('shot-note').textContent = note;
document.getElementById('send-note').textContent = '';
document.getElementById('btn-send').style.display = sent ? 'none' : '';
document.getElementById('btn-done').style.display = sent ? 'none' : '';
document.getElementById('shot-modal').classList.remove('hidden');
}
async function openVacancy(id) {
const ta = document.querySelector(`textarea[data-id="${id}"]`);
const message = ta ? ta.value : (generatedTexts[id] || '');
const note = document.getElementById(`note-${id}`);
if (note) { note.textContent = 'Открываю браузер...'; }
try {
const data = await api(`/api/accounts/${currentAccount}/open`, {
method: 'POST', body: JSON.stringify({ id, message }),
});
if (note) note.textContent = data.note || '';
if (data.screenshot) {
showShotModal(id, data.screenshot, data.note || '', false);
} else {
alert(data.note || 'Не удалось открыть вакансию');
}
} catch (e) {
if (note) note.textContent = 'Ошибка: ' + e.message;
alert('Ошибка: ' + e.message);
}
}
async function sendVacancy(id) {
if (!currentAccount) { alert('Сначала выберите аккаунт'); return; }
const ta = document.querySelector(`textarea[data-id="${id}"]`);
const message = ta ? ta.value : (generatedTexts[id] || '');
const note = document.getElementById(`note-${id}`);
if (note) { note.textContent = 'Отправляю отклик...'; }
try {
const data = await api(`/api/accounts/${currentAccount}/send-now`, {
method: 'POST', body: JSON.stringify({ id, message }),
});
if (note) note.textContent = data.note || '';
if (data.screenshot) {
showShotModal(id, data.screenshot, data.note || 'Отклик отправлен', true);
sentTexts.add(id);
const v = vacancies.find(x => x.id === id);
if (v) v.responded = true;
renderVacancies();
renderTexts();
} else {
alert(data.note || 'Не удалось отправить отклик');
}
} catch (e) {
if (note) note.textContent = 'Ошибка: ' + e.message;
alert('Ошибка: ' + e.message);
}
}
function closeShot() {
document.getElementById('shot-modal').classList.add('hidden');
}
async function confirmSend() {
if (!currentAccount) { alert('Сначала выберите аккаунт'); return; }
const btn = document.getElementById('btn-send');
const note = document.getElementById('send-note');
btn.disabled = true; note.textContent = 'Отправляю...';
try {
const data = await api(`/api/accounts/${currentAccount}/send`, {
method: 'POST', body: JSON.stringify({ id: currentShotId }),
});
note.textContent = data.note || '';
if (data.ok) {
const v = vacancies.find(x => x.id === currentShotId);
if (v) v.responded = true;
sentTexts.add(currentShotId);
renderVacancies();
renderTexts();
}
} catch (e) {
note.textContent = 'Ошибка: ' + e.message;
}
btn.disabled = false;
}
async function markDone(id) {
if (!currentAccount) { alert('Сначала выберите аккаунт'); return; }
await api(`/api/accounts/${currentAccount}/done`, {
method: 'POST', body: JSON.stringify({ id }),
});
const v = vacancies.find(x => x.id === id);
if (v) v.responded = true;
sentTexts.add(id);
renderVacancies();
renderTexts();
const note = document.getElementById(`note-${id}`);
if (note) note.textContent = 'Отмечено как отправленное';
}
async function resetProgress() {
if (!confirm('Сбросить прогресс (все вакансии снова будут «новыми»)?')) return;
await api(`/api/accounts/${currentAccount}/reset`, { method: 'POST' });
await runSearch();
}
async function openLogin() {
if (!currentAccount) return;
try {
const data = await api(`/api/accounts/${currentAccount}/login`);
alert(data.note || 'Вход через cookies');
} catch (e) {
alert('Ошибка: ' + e.message);
}
}
// ---------- Старт ----------
loadAccounts();
</script>
</body>
</html>