Анализ сайта ip-shka.ru
Основное Готовность: 75%
Домен
ip-shka.ru
Состояние доменного имени
?
Проверяем корректность доменного имени и наличие технических проблем на уровне домена.
Домен второго уровня идеален для продвижения.
Отличный запоминающийся домен.
Регистратор домена - SELECTEL-RU
Дата регистрации домена 30.07.2026 (более 2 недель назад)
Ответ сервера
200 Успешный ответ
HTTP-код ответа и цепочка редиректов
?
Код 200 — страница доступна. Коды 3xx — редиректы (цепочки замедляют загрузку и размывают ссылочный вес). Коды 4xx/5xx — ошибки, поисковик не сможет проиндексировать страницу.
Сервер настроен корректно.
Безопасность
Сайт небезопасен
Использование HTTPS и SSL-сертификат
?
HTTPS — обязательный стандарт. Google и Яндекс отдают предпочтение защищённым сайтам. Отсутствие SSL или просроченный сертификат ведут к предупреждениям в браузере и снижению позиций.
На сайте не найден защищенный протокол ssl и сайт не открывается по https!
Не настроен HSTS (Strict-Transport-Security) — рекомендуется включить.
Поздравляем! Сайт не содержится в реестре РКН.
Кодировка
utf-8
Кодировка символов страницы
?
Стандарт — UTF-8. Неправильная кодировка вызывает нечитаемые символы и мешает поисковику корректно распознать текст страницы.
Указана кодировка на странице utf-8.
Язык
ru
Атрибут lang в HTML-теге
?
Атрибут lang (<html lang="ru">) сообщает поисковикам и браузерам, на каком языке написана страница. Помогает при ранжировании в региональном поиске.
Указан язык документа ru.
Скорость загрузки
~0,12сек
Время отклика сервера (TTFB)
?
Time To First Byte — время до получения первого байта от сервера. Норма до 200 мс. Медленный отклик ухудшает пользовательский опыт и ранжирование: Яндекс и Google учитывают скорость страниц.
Скорость загрузки сайта 0,12сек оптимальна.
Объем документа
65Кб
Размер HTML-кода страницы
?
Слишком большой HTML замедляет парсинг браузером и сканирование поисковым роботом. Рекомендуется не более 200 Кб.
Объем html-документа 65Кб оптимален.
Структура html-документа корректна.
Ресурсы
Ресурсы: 3
Внешние ресурсы страницы (CSS, JS, изображения)
?
Количество и тип подключённых ресурсов влияют на скорость загрузки. Большое число запросов увеличивает время рендеринга страницы.
Кол-во файлов ресурсов 3 достаточно.
Показать полный список ресурсов
| Тип | Название | Значение |
|---|---|---|
| stylesheet | https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css | |
| stylesheet | https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css | |
| js | https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js |
Серверные заголовки
Кол-во: 7
HTTP-заголовки ответа сервера
?
Заголовки сервера передают браузеру и поисковику служебную информацию: кеширование, безопасность (CSP, HSTS), сжатие (gzip). Правильная настройка ускоряет загрузку и повышает защищённость.
Найдены серверные заголовки 7шт. Подробнее про серверные заголовки.
Показать полный список серверных заголовков
| Ключ | Значение |
|---|---|
| Server | nginx/1.24.0 |
| Date | Sat, 15 Aug 2026 20:51:12 GMT |
| Transfer-Encoding | chunked |
| Connection | keep-alive |
| Set-Cookie | PHPSESSID=80p8lci596h593buqhsop8ue1p; path=/ |
| Cache-Control | no-store, must-revalidate, no-cache |
| Pragma | no-cache |
CMS
Не определена
Система управления сайтом (движок)
?
CMS — это движок, на котором работает сайт (WordPress, 1C-Bitrix, Tilda и др.). Знание CMS помогает понять возможности SEO-оптимизации и подобрать подходящие инструменты. «Не определена» — вероятно, самописный сайт или нестандартная сборка.
CMS не определена. Вероятно, сайт самописный либо движок надёжно скрыт. Это не ошибка.
Веб-сервер
Программное обеспечение сервера
?
Веб-сервер — это ПО, которое отдаёт страницы посетителям (nginx, Apache, IIS, LiteSpeed и др.). Определяется по серверным заголовкам ответа (Server, X-Powered-By и т.п.). «Не определён» — сервер намеренно скрывает эти заголовки, это нормальная практика безопасности.
Сайт работает на веб-сервере nginx.
Мета-теги Готовность: 32%
Title
Контроль доходов и налогов
Заголовок страницы в браузере и поисковой выдаче
?
Title — главный SEO-заголовок страницы. Влияет на CTR в поиске и ранжирование. Оптимальная длина: 50–70 символов. Ключевые слова — ближе к началу.
Необходимо увеличить число символов в title (текущее значение: 26, оптимально: от 40 до 45)
Дублей словоформ в title не найдено.
Description
Описание страницы в поисковой выдаче (сниппет)
?
Meta Description — текст под заголовком в выдаче. Напрямую на позиции не влияет, но влияет на CTR. Оптимальная длина: 120–160 символов.
Установите мета-тег description!
Keywords
Список ключевых слов страницы (устаревший тег)
?
Meta Keywords не учитывается Яндексом и Google для ранжирования с 2009–2012 годов. Заполнение не обязательно, но не вредит. Конкурент может использовать содержимое для анализа.
Установите мета-тег keywords!
Канонический Url
Указывает поисковику основную версию страницы
?
Canonical (rel=canonical) предотвращает проблему дублей страниц. Должен точно совпадать с URL проверяемой страницы. Неправильный canonical может передать ссылочный вес на другую страницу.
Рекомендуем прописать канонический Url.
Robots
Ошибок нет
Директивы для поисковых роботов на уровне страницы
?
Meta Robots управляет индексацией конкретной страницы: index/noindex — индексировать ли, follow/nofollow — следовать ли по ссылкам. Noindex полностью исключает страницу из поиска.
Meta-тег robots не указан. Страница свободна для индексации.
Адаптивность
width=device-width, initial-scale=1
Настройка масштабирования на мобильных устройствах
?
Тег viewport (<meta name="viewport">) сообщает браузеру, как масштабировать страницу на мобильных. Стандарт: width=device-width, initial-scale=1. Отсутствие — признак отсутствия мобильной версии.
Meta-тег viewport со значением-константой width=device-width задаёт ширину страницы в соответствии с размером экрана.
Meta-тег viewport со значением initial-scale=1.0 определяет масштаб 1:1, т.е. «не масштабировать».
Разметка OpenGraph
Не найдено
Мета-теги для красивых превью в соцсетях
?
OpenGraph (og:title, og:description, og:image) управляет тем, как страница выглядит при репосте в социальных сетях и мессенджерах. Отсутствие OG-тегов — невзрачный превью при шеринге.
Разметка OpenGraph не задана. Страница не оптимизирована под социальные сети. Мета-теги с разметкой Og помогают социальным роботам лучше структурировать Ваш сайт.
Все мета-теги
Кол-во: 1
Полный список мета-тегов страницы
?
Таблица всех meta-тегов, включая нестандартные. Позволяет найти опечатки, дубли и лишние теги.
Найдены мета-теги 1шт. Мета-теги не видимы для человека и предназначены для обмена информацией между веб-страницей и поисковыми системами, браузерами и другими веб-службами. С ними роботы 🤖 и устройства ведут себя более ожидаемо.
Показать полный список мета-тегов
| Тип | Название | Значение |
|---|---|---|
| name | viewport | width=device-width, initial-scale=1 |
Оптимизация Готовность: 62%
Структура
Ошибок нет
Семантические HTML-элементы страницы
?
Проверяет наличие основных структурных элементов: nav, header, footer, main. Корректная семантическая структура помогает поисковику понять архитектуру страницы.
Структура документа корректна (теги <html> и <body> присутствуют по одному на документ).
Контент
Есть ошибки
Объём и качество текстового содержимого
?
Анализирует объём полезного текста на странице. Слишком мало — страница может считаться малополезной. Слишком много — ухудшается читаемость и восприятие.
Среднее число слов в абзаце 6 слишком мало. Сделайте контент более читаемым!
Кол-во слов 335 слишком мало. Добавьте больше текста (минимум 400 слов)!
Слова из title 3 встречаются в тексте достаточно.
Абзацев с текстом 30 достаточно.
Кол-во знаков контента 1780 на странице оптимально.
Заголовки
Ошибок нет
Иерархия заголовков H1–H6
?
H1 должен быть один и содержать ключевой запрос. H2–H6 описывают подразделы. Пропуск уровней (H1 → H3) и несколько H1 — типичные ошибки, снижающие понятность страницы для поисковика.
На странице присутствуют заголовки <h1> 2. Это прекрасно.
На странице присутствуют заголовки <h3> 1.
Тошнота
3,46
Насколько одно слово доминирует в тексте
?
Классическая тошнота = √(частота самого повторяющегося слова). Норма до 7–8: текст воспринимается естественно. Выше — поисковик может счесть страницу переспамленной.
Тошнота превышает норму 3. Измените текст страницы!
Академич. тошнота
4,48%
Насколько текст перенасыщен ключевыми словами
?
Академическая тошнота = (частота слова / общее количество слов) × 100%. Показывает долю конкретного слова в тексте. Норма 5–15%.
Академическая тошнота не дотягивает до нормы 5-15%. Измените текст страницы!
Семантическое ядро
20
Наиболее часто встречающиеся слова на странице
?
Топ слов по частоте использования. Показывает, какие слова доминируют в тексте с точки зрения поисковика.
Контент страницы содержит осмысленный текст и слова.
Показать список слов
| Слово | Кол-во | Частота |
|---|---|---|
| расход: | 12 | 3,58% |
| отмена | 4 | 1,19% |
| доходы: | 4 | 1,19% |
| расходы: | 4 | 1,19% |
| уплате: | 4 | 1,19% |
| уплачено: | 4 | 1,19% |
| изменить | 4 | 1,19% |
| выписки | 3 | 0,90% |
| сохранить | 3 | 0,90% |
| контроль | 2 | 0,60% |
| доходов | 2 | 0,60% |
| регистрация | 2 | 0,60% |
| личный | 2 | 0,60% |
| кабинет | 2 | 0,60% |
| загрузить | 2 | 0,60% |
| уплачено | 2 | 0,60% |
| пароль | 2 | 0,60% |
| автоматически | 2 | 0,60% |
| загружаться | 2 | 0,60% |
| загружайте | 1 | 0,30% |
Индексация Готовность: 80%
Индексирование
Ошибок нет
Разрешено ли индексирование страницы
?
Проверяет, не закрыта ли страница от индексации через robots.txt, meta robots или X-Robots-Tag. Страница, закрытая от индексации, не появится в поисковой выдаче.
Анкоров на странице 5 оптимально. Поисковые роботы обязательно проиндексируют сайт.
Robots.txt
Найден корректный robots.txt
Файл управления сканированием сайта роботами
?
Robots.txt указывает поисковым роботам, какие страницы сканировать, а какие — нет. Ошибки в файле могут случайно закрыть важные разделы от индексации.
Robots.txt настроен корректно. Размер файла: 66841 байт. Загружен за: 0сек.
Проверяемая страница не запрещена в robots.txt.
Robots.txt доступен по постоянному адресу
Показать содержимое robots.txt
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Контроль доходов и налогов</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
</head>
<body class="bg-light">
<!-- Навигация -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary sticky-top">
<div class="container">
<a class="navbar-brand fw-bold" href="#">Контроль доходов <span class="badge bg-warning text-dark ms-1" style="font-size:0.55rem;vertical-align:top;" title="Бета-версия. Возможны доработки.">β</span></a>
<div class="navbar-nav ms-auto">
<button class="btn btn-outline-light me-2" data-bs-toggle="modal" data-bs-target="#loginModal">
<i class="bi bi-box-arrow-in-right me-1"></i>Войти</button>
<button class="btn btn-light" data-bs-toggle="modal" data-bs-target="#registerModal"><i class="bi bi-person-plus me-1"></i>Регистрация</button>
</div>
</div>
</nav>
<div class="container pt-3 pb-5">
<!-- Приветствие для неавторизованных -->
<div id="welcomeSection" class="row mb-5">
<div class="col-12 text-center">
<h1 class="display-5 fw-bold text-primary">Контроль доходов</h1>
<p class="lead">Загружайте выписки и отслеживайте квартальные итоги</p>
<div class="alert alert-info mt-4">
<i class="bi bi-info-circle me-2"></i>Авторизуйтесь для доступа к личному кабинету
</div>
</div>
</div>
<!-- Личный кабинет (скрыт до авторизации) -->
<div id="dashboardSection" class="d-none">
<!-- Заголовок кабинета -->
<div class="row mb-3">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center flex-wrap">
<div>
<h1 class="display-5 fw-bold text-primary mb-1">Личный кабинет</h1>
<p class="lead mb-0 text-muted">Добро пожаловать, <span id="userName"></span>!</p>
</div>
<div class="d-flex align-items-center gap-2 mt-2 mt-md-0">
<span class="text-muted small text-nowrap">Рабочий год:</span>
<select id="working-year-selector" class="form-select form-select-sm" style="width:110px;">
</select>
</div>
</div>
</div>
</div>
<!-- Поле загрузки выписки -->
<div class="row mb-5">
<div class="col-lg-8 mx-auto">
<div class="card shadow">
<div class="card-body text-center p-5">
<h3 class="card-title mb-4">Загрузить банковскую выписку</h3>
<form id="uploadForm" enctype="multipart/form-data">
<div class="mb-3">
<label for="bankFile" class="form-label fw-bold">Выберите файл (TXT)</label>
<input type="file" class="form-control form-control-lg" id="bankFile" accept=".txt" required>
</div>
<button type="submit" class="btn btn-success btn-lg px-5">Загрузить и разобрать доходы</button>
</form>
<div id="uploadStatus" class="mt-3"></div>
<div hidden id="upload_id"></div>
</div>
</div>
</div>
</div>
<!-- Панель страховых взносов -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm border-info">
<div class="card-body py-2">
<!-- режим просмотра -->
<div id="insurance-view" class="d-flex justify-content-between align-items-center flex-wrap">
<div class="d-flex align-items-center gap-3">
<span class="fw-bold text-nowrap">
<i class="bi bi-shield-check me-1"></i>Страховые взносы <span id="insurance-year"></span>:
</span>
<span id="insurance-amount" class="fw-bold text-info">0 ₽</span>
<span class="text-muted small text-nowrap">
За квартал: <span id="insurance-quarterly">0 ₽</span>
</span>
</div>
<div class="d-flex align-items-center gap-2 mt-1 mt-md-0">
<span class="small text-muted text-nowrap">Метод расчёта:</span>
<select id="method-selector" class="form-select form-select-sm" style="width:auto;">
<option value="1">Поквартально (1/4 зачёта)</option>
<option value="2">Накопительный зачёт</option>
</select>
<button id="insurance-edit-btn" class="btn btn-sm btn-outline-secondary" title="Изменить сумму взносов">
<i class="bi bi-pencil"></i>
</button>
</div>
</div>
<!-- режим редактирования (скрыт) -->
<div id="insurance-edit" class="d-none d-flex justify-content-between align-items-center flex-wrap">
<div class="d-flex align-items-center gap-2">
<span class="fw-bold text-nowrap">Сумма за год:</span>
<input type="number" id="insurance-input" class="form-control form-control-sm" style="width:140px;" min="0" step="0.01" placeholder="57390">
<span class="text-muted">₽</span>
<span class="small text-muted ms-2 text-nowrap">Метод:</span>
<select id="method-input" class="form-select form-select-sm" style="width:auto;">
<option value="1">Поквартально</option>
<option value="2">Накопительно</option>
</select>
</div>
<div class="d-flex gap-2 mt-1 mt-md-0">
<button id="insurance-save-btn" class="btn btn-sm btn-primary"><i class="bi bi-check-lg me-1"></i>Сохранить</button>
<button id="insurance-cancel-btn" class="btn btn-sm btn-outline-secondary">Отмена</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Панель управления глобальными дефолтами взносов (только админ) -->
<!-- Квартальные карточки -->
<div class="row g-4">
<!-- Q1 -->
<div class="col-md-6 col-lg-3">
<!-- КАРТОЧКА с collapse-триггером -->
<div class="card shadow hover-shadow">
<!-- Заголовок карточки как триггер collapse -->
<div
class="card-body text-center p-4"
data-bs-toggle="collapse"
data-bs-target="#collapseQ1"
style="cursor: pointer;">
<div class="display-6 text-primary mb-2">1 КВ.</div>
<h5 class="card-title">Январь - Март</h5>
<p class="card-text text-muted small">Доходы: <span id="q1-total">0 ₽</span></p>
<p class="card-text text-danger small mb-0 tax-15-row" style="display:none;">Расходы: <span id="q1-expense">0 ₽</span></p>
<p class="card-text text-muted small mb-0 tax-15-row" style="display:none;">База: <span id="q1-base">0 ₽</span></p>
<p class="card-text text-success small mb-0 fw-bold">Налог <span class="tax-rate-label">6%</span>: <span id="q1-tax">0 ₽</span></p>
<p class="card-text text-info small mb-0">К уплате: <span id="q1-tax-net">0 ₽</span></p>
<p class="card-text text-secondary small mb-0 mt-1">Уплачено: <span id="q1-tax-paid" class="fw-bold text-decoration-underline" style="cursor:pointer; text-decoration-style:dashed;" title="Нажмите чтобы изменить"><i class="bi bi-pencil-square text-muted me-1" style="font-size:0.7rem;"></i>0 ₽</span></p>
</div>
<!-- Кнопка ИЗМЕНИТЬ внутри карточки, но без data-bs-toggle -->
<div class="card-footer text-center">
<a href="tax-editor.php?quarter=1"
class="btn btn-sm btn-outline-primary">
Изменить
</a>
</div>
</div>
<!-- Список по месяцам -->
<div class="collapse" id="collapseQ1">
<div class="card card-body border-0 mt-0">
<ul class="list-unstyled">
<li class="p-3 border-bottom"><strong>Январь:</strong> <span id="jan-total">0 ₽</span></li>
<li class="p-3 border-bottom tax-15-row" style="display:none;"><small class="text-danger">Расход:</small> <span id="jan-expense">0 ₽</span></li>
<li class="p-3 border-bottom"><strong>Февраль:</strong> <span id="feb-total">0 ₽</span></li>
<li class="p-3 border-bottom tax-15-row" style="display:none;"><small class="text-danger">Расход:</small> <span id="feb-expense">0 ₽</span></li>
<li class="p-3"><strong>Март:</strong> <span id="mar-total">0 ₽</span></li>
<li class="p-3 tax-15-row" style="display:none;"><small class="text-danger">Расход:</small> <span id="mar-expense">0 ₽</span></li>
</ul>
</div>
</div>
</div>
<!-- Q2 -->
<div class="col-md-6 col-lg-3">
<div class="card shadow hover-shadow">
<div
class="card-body text-center p-4"
data-bs-toggle="collapse"
data-bs-target="#collapseQ2"
style="cursor: pointer;"
>
<div class="display-6 text-success mb-2">2 КВ.</div>
<h5 class="card-title">Апрель - Июнь</h5>
<p class="card-text text-muted small">Доходы: <span id="q2-total">0 ₽</span></p>
<p class="card-text text-danger small mb-0 tax-15-row" style="display:none;">Расходы: <span id="q2-expense">0 ₽</span></p>
<p class="card-text text-muted small mb-0 tax-15-row" style="display:none;">База: <span id="q2-base">0 ₽</span></p>
<p class="card-text text-success small mb-0 fw-bold">Налог <span class="tax-rate-label">6%</span>: <span id="q2-tax">0 ₽</span></p>
<p class="card-text text-info small mb-0">К уплате: <span id="q2-tax-net">0 ₽</span></p>
<p class="card-text text-secondary small mb-0 mt-1">Уплачено: <span id="q2-tax-paid" class="fw-bold text-decoration-underline" style="cursor:pointer; text-decoration-style:dashed;" title="Нажмите чтобы изменить"><i class="bi bi-pencil-square text-muted me-1" style="font-size:0.7rem;"></i>0 ₽</span></p>
</div>
<div class="card-footer text-center">
<a href="tax-editor.php?quarter=2" class="btn btn-sm btn-outline-primary">Изменить</a>
</div>
</div>
<div class="collapse" id="collapseQ2">
<div class="card card-body border-0 mt-0">
<ul class="list-unstyled">
<li class="p-3 border-bottom"><strong>Апрель:</strong> <span id="apr-total">0 ₽</span></li>
<li class="p-3 border-bottom tax-15-row" style="display:none;"><small class="text-danger">Расход:</small> <span id="apr-expense">0 ₽</span></li>
<li class="p-3 border-bottom"><strong>Май:</strong> <span id="may-total">0 ₽</span></li>
<li class="p-3 border-bottom tax-15-row" style="display:none;"><small class="text-danger">Расход:</small> <span id="may-expense">0 ₽</span></li>
<li class="p-3"><strong>Июнь:</strong> <span id="jun-total">0 ₽</span></li>
<li class="p-3 tax-15-row" style="display:none;"><small class="text-danger">Расход:</small> <span id="jun-expense">0 ₽</span></li>
</ul>
</div>
</div>
</div>
<!-- Q3 -->
<div class="col-md-6 col-lg-3">
<div class="card shadow hover-shadow">
<div
class="card-body text-center p-4"
data-bs-toggle="collapse"
data-bs-target="#collapseQ3"
style="cursor: pointer;"
>
<div class="display-6 text-warning mb-2">3 КВ.</div>
<h5 class="card-title">Июль - Сентябрь</h5>
<p class="card-text text-muted small">Доходы: <span id="q3-total">0 ₽</span></p>
<p class="card-text text-danger small mb-0 tax-15-row" style="display:none;">Расходы: <span id="q3-expense">0 ₽</span></p>
<p class="card-text text-muted small mb-0 tax-15-row" style="display:none;">База: <span id="q3-base">0 ₽</span></p>
<p class="card-text text-success small mb-0 fw-bold">Налог <span class="tax-rate-label">6%</span>: <span id="q3-tax">0 ₽</span></p>
<p class="card-text text-info small mb-0">К уплате: <span id="q3-tax-net">0 ₽</span></p>
<p class="card-text text-secondary small mb-0 mt-1">Уплачено: <span id="q3-tax-paid" class="fw-bold text-decoration-underline" style="cursor:pointer; text-decoration-style:dashed;" title="Нажмите чтобы изменить"><i class="bi bi-pencil-square text-muted me-1" style="font-size:0.7rem;"></i>0 ₽</span></p>
</div>
<div class="card-footer text-center">
<a href="tax-editor.php?quarter=3" class="btn btn-sm btn-outline-primary">Изменить</a>
</div>
</div>
<div class="collapse" id="collapseQ3">
<div class="card card-body border-0 mt-0">
<ul class="list-unstyled">
<li class="p-3 border-bottom"><strong>Июль:</strong> <span id="jul-total">0 ₽</span></li>
<li class="p-3 border-bottom tax-15-row" style="display:none;"><small class="text-danger">Расход:</small> <span id="jul-expense">0 ₽</span></li>
<li class="p-3 border-bottom"><strong>Август:</strong> <span id="aug-total">0 ₽</span></li>
<li class="p-3 border-bottom tax-15-row" style="display:none;"><small class="text-danger">Расход:</small> <span id="aug-expense">0 ₽</span></li>
<li class="p-3"><strong>Сентябрь:</strong> <span id="sep-total">0 ₽</span></li>
<li class="p-3 tax-15-row" style="display:none;"><small class="text-danger">Расход:</small> <span id="sep-expense">0 ₽</span></li>
</ul>
</div>
</div>
</div>
<!-- Q4 -->
<div class="col-md-6 col-lg-3">
<div class="card shadow hover-shadow">
<div
class="card-body text-center p-4"
data-bs-toggle="collapse"
data-bs-target="#collapseQ4"
style="cursor: pointer;"
>
<div class="display-6 text-danger mb-2">4 КВ.</div>
<h5 class="card-title">Октябрь - Декабрь</h5>
<p class="card-text text-muted small">Доходы: <span id="q4-total">0 ₽</span></p>
<p class="card-text text-danger small mb-0 tax-15-row" style="display:none;">Расходы: <span id="q4-expense">0 ₽</span></p>
<p class="card-text text-muted small mb-0 tax-15-row" style="display:none;">База: <span id="q4-base">0 ₽</span></p>
<p class="card-text text-success small mb-0 fw-bold">Налог <span class="tax-rate-label">6%</span>: <span id="q4-tax">0 ₽</span></p>
<p class="card-text text-warning small mb-0 tax-15-row" style="display:none;">Мин. налог 1%: <span id="q4-min-tax">0 ₽</span></p>
<p class="card-text text-info small mb-0">К уплате: <span id="q4-tax-net">0 ₽</span></p>
<p class="card-text text-secondary small mb-0 mt-1">Уплачено: <span id="q4-tax-paid" class="fw-bold text-decoration-underline" style="cursor:pointer; text-decoration-style:dashed;" title="Нажмите чтобы изменить"><i class="bi bi-pencil-square text-muted me-1" style="font-size:0.7rem;"></i>0 ₽</span></p>
</div>
<div class="card-footer text-center">
<a href="tax-editor.php?quarter=4" class="btn btn-sm btn-outline-primary">Изменить</a>
</div>
</div>
<div class="collapse" id="collapseQ4">
<div class="card card-body border-0 mt-0">
<ul class="list-unstyled">
<li class="p-3 border-bottom"><strong>Октябрь:</strong> <span id="oct-total">0 ₽</span></li>
<li class="p-3 border-bottom tax-15-row" style="display:none;"><small class="text-danger">Расход:</small> <span id="oct-expense">0 ₽</span></li>
<li class="p-3 border-bottom"><strong>Ноябрь:</strong> <span id="nov-total">0 ₽</span></li>
<li class="p-3 border-bottom tax-15-row" style="display:none;"><small class="text-danger">Расход:</small> <span id="nov-expense">0 ₽</span></li>
<li class="p-3"><strong>Декабрь:</strong> <span id="dec-total">0 ₽</span></li>
<li class="p-3 tax-15-row" style="display:none;"><small class="text-danger">Расход:</small> <span id="dec-expense">0 ₽</span></li>
</ul>
</div>
</div>
</div>
<!-- ?? div -->
</div>
</div>
</div>
<!-- Модальное окно редактирования уплаченного налога -->
<div class="modal fade" id="taxPaidModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header bg-info text-white">
<h5 class="modal-title">Уплачено — <span id="taxPaidQuarterLabel"></span></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label small">К уплате по расчёту: <strong id="taxPaidCalculated"></strong></label>
</div>
<div class="mb-3">
<label for="taxPaidInput" class="form-label">Фактически уплачено (₽)</label>
<input type="number" class="form-control form-control-lg text-center" id="taxPaidInput" min="0" step="0.01" placeholder="0.00">
</div>
<div id="taxPaidStatus" class="small"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-info text-white" id="taxPaidSaveBtn">Сохранить</button>
</div>
</div>
</div>
</div>
<!-- Модальное окно входа -->
<div class="modal fade" id="loginModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title"><i class="bi bi-box-arrow-in-right me-2"></i>Вход в личный кабинет</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="loginForm">
<div class="mb-3">
<label for="loginEmail" class="form-label">Email</label>
<input type="email" class="form-control" id="loginEmail" required>
</div>
<div class="mb-4">
<label for="loginPassword" class="form-label" >Пароль</label>
<input type="password" class="form-control" id="loginPassword" required>
</div>
<button type="submit" class="btn btn-primary w-100">Войти</button>
</form>
<div id="loginStatus" class="mt-3"></div>
</div>
</div>
</div>
</div>
<!-- Модальное окно регистрации -->
<div class="modal fade" id="registerModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title"><i class="bi bi-person-plus me-2"></i>Регистрация</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="registerForm">
<div class="mb-3">
<label for="regName" class="form-label">Имя</label>
<input type="text" class="form-control" id="regName" required>
</div>
<div class="mb-3">
<label for="regEmail" class="form-label">Email</label>
<input type="email" class="form-control" id="regEmail" required>
</div>
<div class="mb-4">
<label for="regPassword" class="form-label">Пароль (мин. 6 символов)</label>
<input type="password" class="form-control" id="regPassword" minlength="6" required>
</div>
<!-- Honeypot: невидимое поле для ботов, не заполняйте -->
<div style="position:absolute;left:-9999px;" aria-hidden="true">
<input type="text" name="_website" id="_website" tabindex="-1" autocomplete="off">
</div>
<input type="hidden" id="_filled_at" name="_filled_at">
<button type="submit" class="btn btn-success w-100" id="registerSubmitBtn">Зарегистрироваться</button>
</form>
<div id="registerStatus" class="mt-3"></div>
</div>
</div>
</div>
</div>
<!-- Модальное окно настроек профиля -->
<div class="modal fade" id="profileModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title"><i class="bi bi-person-gear me-2"></i>Настройки профиля</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="profileForm">
<div class="mb-3">
<label for="profileInn" class="form-label">ИНН</label>
<input type="text" class="form-control" id="profileInn" placeholder="123456789012" maxlength="12" pattern="[0-9]{10,12}">
<div class="form-text">10 или 12 цифр</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Ставка налога УСН</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="taxRate" id="taxRate6" value="6">
<label class="form-check-label" for="taxRate6">
<strong>6%</strong> — «Доходы»
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="taxRate" id="taxRate15" value="15">
<label class="form-check-label" for="taxRate15">
<strong>15%</strong> — «Доходы минус расходы»
</label>
</div>
</div>
</div>
<hr>
<div class="mb-0">
<label class="form-label fw-bold"><i class="bi bi-bank me-1"></i>Мои банковские счета</label>
<div class="small text-muted mb-2">Выписки с этих счетов будут автоматически загружаться при получении на почту.</div>
<div id="bank-accounts-list" class="mb-2"></div>
<div class="d-flex gap-2">
<input type="text" id="bank-account-input" class="form-control form-control-sm" style="max-width:260px;" placeholder="40802810767170001604" maxlength="20">
<button type="button" id="bank-account-add-btn" class="btn btn-sm btn-primary"><i class="bi bi-plus-lg me-1"></i>Добавить</button>
</div>
<div id="bank-account-status" class="small mt-1"></div>
</div>
<div id="profileStatus" class="mb-0 mt-3"></div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="profileSaveBtn">
<i class="bi bi-check-lg me-1"></i>Сохранить
</button>
</div>
</div>
</div>
</div>
<!-- Модальное окно подтверждения удаления счёта -->
<div class="modal fade" id="bankDeleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2"></i>Удаление счёта</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="mb-1">Отвязать счёт <strong id="bank-delete-label"></strong>?</p>
<small class="text-muted">Выписки с этого счёта перестанут загружаться автоматически.</small>
<div id="bank-delete-status" class="mt-2"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-danger" id="bankDeleteConfirmBtn">
<i class="bi bi-trash me-1"></i>Удалить
</button>
</div>
</div>
</div>
</div>
<!-- Модальное окно подтверждения email -->
<div class="modal fade" id="verifyModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header bg-info text-white">
<h5 class="modal-title"><i class="bi bi-envelope-check me-2"></i>Подтверждение email</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="small">На <strong id="verify-email-label"></strong> отправлен код подтверждения.</p>
<p class="small text-muted">Введите 6-значный код из письма:</p>
<div class="mb-3">
<input type="text" class="form-control form-control-lg text-center" id="verifyCodeInput"
placeholder="000000" maxlength="6" inputmode="numeric" pattern="[0-9]{6}" autocomplete="one-time-code"
style="font-size:1.8rem;letter-spacing:8px;">
</div>
<div id="verifyStatus" class="small"></div>
<input type="hidden" id="verifyEmailHidden">
</div>
<div class="modal-footer d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary btn-sm" id="verifyResendBtn">
<i class="bi bi-arrow-repeat me-1"></i>Отправить код повторно
</button>
<button type="button" class="btn btn-info text-white" id="verifyConfirmBtn">
<i class="bi bi-check-lg me-1"></i>Подтвердить
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<style>
.hover-shadow:hover { box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15) !important; }
.card { cursor: pointer; transition: all 0.2s; }
.navbar-brand { font-size: 1.5rem; }
.verification-highlight { animation: verifyPulse 0.4s ease-in-out 3; }
@keyframes verifyPulse { 0%,100% { box-shadow: 0 0 0 0 rgba(13,202,240,0.4); } 50% { box-shadow: 0 0 0 8px rgba(13,202,240,0); } }
</style>
<script>
// Проверка сессии при загрузке страницы
function checkSession() {
const token = localStorage.getItem('authToken');
const userName = localStorage.getItem('userName');
if (token && userName) {
document.getElementById('welcomeSection').classList.add('d-none');
document.getElementById('dashboardSection').classList.remove('d-none');
document.getElementById('userName').textContent = userName;
}
}
function pollStatus(uploadId) {
const interval = setInterval(async () => {
const res = await fetch('api/status.php?upload_id=' + encodeURIComponent(uploadId));
const json = await res.json();
// обновить UI: json.status, json.progress, json.message
if (json.status === 'done' || json.status === 'failed' || json.status==='unknown') {
const status = document.getElementById('uploadStatus');
status.innerHTML = '<div class="alert alert-success">Данные загружены!</div>';
clearInterval(interval);
}
}, 1500); // опрашиваем каждые 1.5 сек
}
// ==========================================
// Рабочий год — глобально для всех операций
// ==========================================
// построить селектор года
function buildYearSelector() {
const sel = document.getElementById('working-year-selector');
if (!sel) return;
const now = new Date().getFullYear();
sel.innerHTML = '';
for (let y = now + 1; y >= now - 5; y--) {
const opt = document.createElement('option');
opt.value = y;
opt.textContent = y;
sel.appendChild(opt);
}
sel.value = getWorkingYear();
sel.addEventListener('change', () => {
localStorage.setItem('workingYear', sel.value);
updateTaxEditorLinks();
loadQuarterTotals();
});
}
// получить текущий рабочий год (из localStorage или системный)
function getWorkingYear() {
const stored = localStorage.getItem('workingYear');
if (stored && !isNaN(stored) && parseInt(stored) >= 2020) return parseInt(stored);
const now = new Date().getFullYear();
localStorage.setItem('workingYear', now);
return now;
}
// перезаписать ссылки на tax-editor с учётом рабочего года
function updateTaxEditorLinks() {
const year = getWorkingYear();
document.querySelectorAll('a[href*="tax-editor.php"]').forEach(link => {
let href = link.getAttribute('href');
href = href.replace(/[&?]year=\d+/, '');
href += (href.includes('?') ? '&' : '?') + 'year=' + year;
link.setAttribute('href', href);
});
}
// Функция загрузки сумм по кварталам
function loadQuarterTotals() {
if (!localStorage.getItem('authToken')) {
return;
}
const year = getWorkingYear();
// синхронизируем селектор с localStorage (защита от рассинхрона)
const sel = document.getElementById('working-year-selector');
if (sel && parseInt(sel.value) !== year) {
sel.value = year;
}
fetch(`api/quarter_totals.php?year=${year}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem('authToken') // или отправь токен как query/cookie
}
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.warn('Не удалось загрузить итоги:', data.error);
return;
}
// Функция для форматирования числа в сумму с ₽
const formatMoney = (val) => {
return new Intl.NumberFormat('ru-RU').format(val || 0) + ' ₽';
};
// месячные итоги
const monthMap = {
1: 'jan-total',
2: 'feb-total',
3: 'mar-total',
4: 'apr-total',
5: 'may-total',
6: 'jun-total',
7: 'jul-total',
8: 'aug-total',
9: 'sep-total',
10: 'oct-total',
11: 'nov-total',
12: 'dec-total'
};
for (let m = 1; m <= 12; m++) {
const id = monthMap[m];
const total = data.monthTotals[m] ?? 0;
const el = document.getElementById(id);
if (el) el.textContent = formatMoney(total);
}
// квартальные доходы
document.getElementById('q1-total').textContent = formatMoney(data.q1 || 0);
document.getElementById('q2-total').textContent = formatMoney(data.q2 || 0);
document.getElementById('q3-total').textContent = formatMoney(data.q3 || 0);
document.getElementById('q4-total').textContent = formatMoney(data.q4 || 0);
// налог 6% (gross) — берём с сервера
const gross = data.gross_tax || {};
document.getElementById('q1-tax').textContent = formatMoney(gross[1] || 0);
document.getElementById('q2-tax').textContent = formatMoney(gross[2] || 0);
document.getElementById('q3-tax').textContent = formatMoney(gross[3] || 0);
document.getElementById('q4-tax').textContent = formatMoney(gross[4] || 0);
// обновляем метки ставки налога в карточках
const taxRate = data.tax_rate || 6;
document.querySelectorAll('.tax-rate-label').forEach(el => {
el.textContent = taxRate + '%';
});
window._taxRate = taxRate;
// Заполняем данные для режима 15%
const monthExpenses = data.monthExpenses || {};
for (let m = 1; m <= 12; m++) {
const id = monthMap[m];
const elExp = document.getElementById(id.replace('total', 'expense'));
if (elExp) elExp.textContent = formatMoney(monthExpenses[m] || 0);
}
const expenses = data.expenses || {};
const cumBase = data.cum_base || {};
const minTax = data.min_tax || {};
for (let q = 1; q <= 4; q++) {
const elExp = document.getElementById('q' + q + '-expense');
if (elExp) elExp.textContent = formatMoney((expenses['q' + q]) || 0);
const elBase = document.getElementById('q' + q + '-base');
if (elBase) elBase.textContent = formatMoney((cumBase['q' + q]) || 0);
}
const elMinTax = document.getElementById('q4-min-tax');
if (elMinTax) elMinTax.textContent = formatMoney(minTax[4] || 0);
// Переключаем видимость строк в зависимости от ставки
toggleTaxModeUI(taxRate);
// страховые взносы
const insurance = data.insurance || {};
document.getElementById('insurance-year').textContent = data.year;
document.getElementById('insurance-amount').textContent = formatMoney(insurance.annual_amount || 0);
document.getElementById('insurance-quarterly').textContent = formatMoney(insurance.quarterly_share || 0);
// синхронизируем селектор метода с БД
const methodFromDB = insurance.calculation_method || 1;
const methodSelector = document.getElementById('method-selector');
if (methodSelector && parseInt(methodSelector.value) !== methodFromDB) {
methodSelector.value = methodFromDB;
}
// сохраняем для переключения метода
window._taxData = data;
// применяем выбранный метод
applySelectedMethod();
})
.catch(err => {
console.error('Ошибка загрузки квартальных итогов:', err);
});
}
// Применить выбранный метод расчёта страховых взносов
function applySelectedMethod() {
const data = window._taxData;
if (!data) return;
const selector = document.getElementById('method-selector');
const method = selector ? parseInt(selector.value) : 1;
// final-значения с учётом уже уплаченных сумм
const netTax = method === 1
? (data.method1_final || data.method1_split)
: (data.method2_final || data.method2_cumulative);
// base-значения (без учёта уплат) — для справки в модалке
window._taxData._netBase = method === 1 ? data.method1_split : data.method2_cumulative;
const formatMoney = (val) => {
return new Intl.NumberFormat('ru-RU').format(val || 0) + ' ₽';
};
for (let q = 1; q <= 4; q++) {
const el = document.getElementById('q' + q + '-tax-net');
if (el) el.textContent = formatMoney((netTax && netTax['q' + q]) || 0);
}
// также обновим уплаченные суммы (если data обновилась)
const paid = data.tax_paid || {};
for (let q = 1; q <= 4; q++) {
const el = document.getElementById('q' + q + '-tax-paid');
if (el) {
el.textContent = formatMoney(paid[q] || 0);
el.dataset.paid = paid[q] || 0;
}
}
}
// === Редактирование уплаченного налога ===
const QUARTER_NAMES = {1:'1 КВ. (Янв-Март)',2:'2 КВ. (Апр-Июн)',3:'3 КВ. (Июл-Сен)',4:'4 КВ. (Окт-Дек)'};
let paidEditQuarter = 0;
// клик по «Уплачено» — открыть модалку
for (let q = 1; q <= 4; q++) {
const el = document.getElementById('q' + q + '-tax-paid');
if (el) {
el.addEventListener('click', () => {
paidEditQuarter = q;
document.getElementById('taxPaidQuarterLabel').textContent = QUARTER_NAMES[q];
document.getElementById('taxPaidInput').value = el.dataset.paid || 0;
// показать "К уплате по расчёту" (base, без учёта уже уплаченного)
const formatMoney = (v) => new Intl.NumberFormat('ru-RU').format(v || 0) + ' ₽';
const base = (window._taxData && window._taxData._netBase) ? (window._taxData._netBase['q' + q] || 0) : 0;
document.getElementById('taxPaidCalculated').textContent = formatMoney(base);
document.getElementById('taxPaidStatus').innerHTML = '';
var modal = new bootstrap.Modal(document.getElementById('taxPaidModal'));
modal.show();
});
}
}
// сохранить уплаченную сумму
document.getElementById('taxPaidSaveBtn').addEventListener('click', function() {
const amount = parseFloat(document.getElementById('taxPaidInput').value) || 0;
const status = document.getElementById('taxPaidStatus');
const btn = this;
const year = getWorkingYear();
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>';
fetch('api/tax_paid.php?year=' + year, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ quarter: paidEditQuarter, paid_amount: amount })
})
.then(r => r.json())
.then(res => {
if (res.success) {
status.innerHTML = '<span class="text-success">Сохранено</span>';
// обновить в DOM сразу
const el = document.getElementById('q' + paidEditQuarter + '-tax-paid');
if (el) {
const formatMoney = (v) => new Intl.NumberFormat('ru-RU').format(v || 0) + ' ₽';
el.textContent = formatMoney(amount);
el.dataset.paid = amount;
}
// обновить window._taxData
if (window._taxData) {
if (!window._taxData.tax_paid) window._taxData.tax_paid = {};
window._taxData.tax_paid[paidEditQuarter] = amount;
}
// Пересчитать все кварталы с учётом новой уплаты
loadQuarterTotals();
setTimeout(() => {
bootstrap.Modal.getOrCreateInstance(document.getElementById('taxPaidModal')).hide();
btn.disabled = false;
btn.innerHTML = 'Сохранить';
status.innerHTML = '';
}, 400);
} else {
status.innerHTML = '<span class="text-danger">' + (res.error || 'Ошибка') + '</span>';
btn.disabled = false;
btn.innerHTML = 'Сохранить';
}
})
.catch(err => {
status.innerHTML = '<span class="text-danger">Ошибка</span>';
console.error(err);
btn.disabled = false;
btn.innerHTML = 'Сохранить';
});
});
// === Редактирование страховых взносов ===
// Кнопка «Изменить» — показать форму
document.getElementById('insurance-edit-btn').addEventListener('click', () => {
const data = window._taxData;
if (data && data.insurance) {
document.getElementById('insurance-input').value = data.insurance.annual_amount || 0;
document.getElementById('method-input').value = data.insurance.calculation_method || 1;
}
document.getElementById('insurance-view').classList.add('d-none');
document.getElementById('insurance-edit').classList.remove('d-none');
});
// Кнопка «Отмена»
document.getElementById('insurance-cancel-btn').addEventListener('click', () => {
document.getElementById('insurance-edit').classList.add('d-none');
document.getElementById('insurance-view').classList.remove('d-none');
});
// Кнопка «Сохранить»
document.getElementById('insurance-save-btn').addEventListener('click', () => {
const amount = parseFloat(document.getElementById('insurance-input').value) || 0;
const method = parseInt(document.getElementById('method-input').value) || 1;
const year = getWorkingYear();
fetch('api/insurance.php?year=' + year, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
annual_amount: amount,
calculation_method: method
})
})
.then(r => r.json())
.then(res => {
if (res.success) {
// скрыть форму, перезагрузить данные
document.getElementById('insurance-edit').classList.add('d-none');
document.getElementById('insurance-view').classList.remove('d-none');
loadQuarterTotals();
} else {
alert('Ошибка: ' + (res.error || 'не удалось сохранить'));
}
})
.catch(err => {
alert('Ошибка соединения');
console.error(err);
});
});
// Сохранение глобального дефолта взносов (только админ)
const globalSaveBtn = document.getElementById('global-save-btn');
if (globalSaveBtn) {
globalSaveBtn.addEventListener('click', () => {
const year = parseInt(document.getElementById('global-year').value) || 0;
const amount = parseFloat(document.getElementById('global-amount').value) || 0;
const status = document.getElementById('global-status');
if (!year || year < 2025) {
status.innerHTML = '<span class="text-danger">Укажите год</span>';
return;
}
if (amount <= 0) {
status.innerHTML = '<span class="text-danger">Укажите сумму</span>';
return;
}
status.innerHTML = '<span class="text-info">Сохранение...</span>';
fetch('api/insurance.php?year=' + year + '&global=1', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ annual_amount: amount, calculation_method: 1 })
})
.then(r => r.json())
.then(res => {
if (res.success) {
status.innerHTML = '<span class="text-success">✅ ' + year + ' год = ' + new Intl.NumberFormat('ru-RU').format(amount) + ' ₽ сохранено</span>';
if (year == getWorkingYear()) {
loadQuarterTotals();
}
} else {
status.innerHTML = '<span class="text-danger">Ошибка: ' + (res.error || 'неизвестно') + '</span>';
}
})
.catch(err => {
status.innerHTML = '<span class="text-danger">Ошибка соединения</span>';
console.error(err);
});
});
}
// Отмечаем время открытия формы регистрации (для honeypot-защиты)
let registerFormOpened = 0;
document.getElementById('registerModal').addEventListener('show.bs.modal', () => {
registerFormOpened = Math.floor(Date.now() / 1000);
});
// Регистрация
document.getElementById('registerForm').addEventListener('submit', function(e) {
e.preventDefault();
const name = document.getElementById('regName').value;
const email = document.getElementById('regEmail').value;
const password = document.getElementById('regPassword').value;
const status = document.getElementById('registerStatus');
const btn = document.getElementById('registerSubmitBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Отправка...';
// AJAX запрос на register.php
fetch('register.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name, email, password,
_website: document.getElementById('_website').value,
_filled_at: registerFormOpened
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Закрываем модалку регистрации и показываем форму подтверждения
bootstrap.Modal.getOrCreateInstance(document.getElementById('registerModal')).hide();
document.getElementById('registerForm').reset();
status.innerHTML = '';
// Показываем окно верификации
showVerifyModal(data.email);
// Если пришёл debug_code — SMTP не отправил, показываем код и ошибку
if (data.debug_code) {
document.getElementById('verifyCodeInput').value = data.debug_code;
let errMsg = '<div class="alert alert-danger py-1 small">';
errMsg += '<b>Письмо не отправлено!</b><br>';
errMsg += 'Ошибка SMTP: ' + (data.mail_error || 'неизвестна') + '<br>';
if (data.mail_debug && data.mail_debug.length > 0) {
errMsg += '<details class="mt-1"><summary>Лог SMTP</summary>';
data.mail_debug.forEach(function(d) { errMsg += d + '<br>'; });
errMsg += '</details>';
}
errMsg += 'Код: <b style="font-size:1.2em;">' + data.debug_code + '</b></div>';
document.getElementById('verifyStatus').innerHTML = errMsg;
}
} else {
status.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
btn.disabled = false;
btn.innerHTML = 'Зарегистрироваться';
}
})
.catch(() => {
status.innerHTML = '<div class="alert alert-danger">Ошибка сервера</div>';
btn.disabled = false;
btn.innerHTML = 'Зарегистрироваться';
});
});
// Вход
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
const email = document.getElementById('loginEmail').value;
const password = document.getElementById('loginPassword').value;
const status = document.getElementById('loginStatus');
// AJAX запрос на login.php
fetch('login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
.then(response => response.json())
.then(data => {
if (data.success) {
localStorage.setItem('authToken', data.token);
localStorage.setItem('userName', data.name);
bootstrap.Modal.getOrCreateInstance(document.getElementById('loginModal')).hide();
checkSession();
document.getElementById('loginForm').reset();
location.reload(true);
} else if (data.needs_verification) {
// Email не подтверждён — показываем форму верификации
bootstrap.Modal.getOrCreateInstance(document.getElementById('loginModal')).hide();
status.innerHTML = '';
showVerifyModal(data.email);
} else {
status.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
}
})
.catch(() => {
status.innerHTML = '<div class="alert alert-danger">Ошибка сервера</div>';
});
});
// Выход
document.addEventListener('DOMContentLoaded', () => {
// Проверяем, скрыт ли welcomeSection
const welcomeSection = document.getElementById('welcomeSection');
if (welcomeSection && welcomeSection.classList.contains('d-none')) {
buildYearSelector();
updateTaxEditorLinks();
loadQuarterTotals();
loadProfile();
setInterval(loadQuarterTotals, 60000);
}
// переключатель метода страховых взносов
const methodSelector = document.getElementById('method-selector');
if (methodSelector) {
methodSelector.addEventListener('change', () => {
const year = getWorkingYear();
const insurance = (window._taxData && window._taxData.insurance) || {};
fetch('api/insurance.php?year=' + year, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
annual_amount: insurance.annual_amount || 0,
calculation_method: parseInt(methodSelector.value)
})
});
applySelectedMethod();
});
}
// загрузка глобального дефолта при открытии панели (только админ)
const globalPanel = document.getElementById('globalInsurancePanel');
if (globalPanel) {
function loadGlobalDefault() {
const year = parseInt(document.getElementById('global-year').value) || new Date().getFullYear();
fetch('api/insurance.php?year=' + year + '&global=1', { method: 'GET' })
.then(r => r.json())
.then(data => {
if (data.success && data.exists !== false) {
document.getElementById('global-amount').value = data.annual_amount || 0;
} else {
document.getElementById('global-amount').value = '';
}
});
}
globalPanel.addEventListener('shown.bs.collapse', loadGlobalDefault);
const globalYearInput = document.getElementById('global-year');
if (globalYearInput) {
globalYearInput.addEventListener('change', loadGlobalDefault);
}
}
const logoutBtn = document.getElementById('logoutBtn');
// Проверяем наличие кнопки, чтобы не возникло ошибки, если её нет на странице
if (logoutBtn) {
logoutBtn.addEventListener('click', () => {
fetch('logout.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: localStorage.getItem('authToken') })
})
.finally(() => {
// Очищаем данные в любом случае (успех или ошибка),
// чтобы пользователь гарантированно вышел
localStorage.removeItem('authToken');
localStorage.removeItem('userName');
// Обновляем интерфейс, если элементы существуют
const welcomeSection = document.getElementById('welcomeSection');
const dashboardSection = document.getElementById('dashboardSection');
if (welcomeSection) welcomeSection.classList.remove('d-none');
if (dashboardSection) dashboardSection.classList.add('d-none');
// Перезагружаем страницу
location.reload(true);
});
});
}
});
// Обработка загрузки файла (только для авторизованных)
document.getElementById('uploadForm').addEventListener('submit', function(e) {
e.preventDefault();
if (!localStorage.getItem('authToken')) {
alert('Необходимо авторизоваться');
return;
}
const file = document.getElementById('bankFile').files[0];
const status = document.getElementById('uploadStatus');
const upload_id = document.getElementById('upload_id');
if (file) {
const formData = new FormData();
formData.append('file', file);
formData.append('token', localStorage.getItem('authToken'));
status.innerHTML = '<div class="alert alert-info">Обработка файла...</div>';
fetch('upload.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
document.getElementById('bankFile').value = '';
if (data.success) {
status.innerHTML = '<div class="alert alert-success">Файл успешно обработан и данные сохранены!</div>';
upload_id.innerHTML = data.upload_id;
pollStatus(data.upload_id);
} else {
status.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
}
})
.catch(err => {
document.getElementById('bankFile').value = '';
status.innerHTML = '<div class="alert alert-danger">Ошибка сервера</div>';
console.error(err);
});
}
});
// ==========================================
// Настройки профиля
// ==========================================
// Загрузить настройки профиля с сервера
function loadProfile() {
fetch('api/profile.php', { method: 'GET' })
.then(r => r.json())
.then(data => {
if (data.success && data.profile) {
document.getElementById('profileInn').value = data.profile.inn || '';
const rate = data.profile.tax_rate || 6;
document.getElementById('taxRate' + rate).checked = true;
// сразу переключаем UI под выбранную ставку
toggleTaxModeUI(rate);
}
})
.catch(err => console.error('Ошибка загрузки профиля:', err));
}
// Обновить подписи "Налог X%" в карточках кварталов и видимость 15% строк
function toggleTaxModeUI(rate) {
const is15 = (rate === 15);
// строки, видимые только для 15% (расходы, база, мин.налог)
document.querySelectorAll('.tax-15-row').forEach(el => {
el.style.display = is15 ? '' : 'none';
});
// селектор метода страховых взносов — скрываем при 15%
const methodSelector = document.getElementById('method-selector');
if (methodSelector && methodSelector.parentElement) {
methodSelector.parentElement.style.display = is15 ? 'none' : '';
}
// подпись "Метод расчёта"
const methodLabel = document.querySelector('#insurance-view .d-flex .text-muted.small');
// ищем span с текстом "Метод расчёта:"
const methodLabels = document.querySelectorAll('#insurance-view span');
methodLabels.forEach(span => {
if (span.textContent.includes('Метод расчёта')) {
span.style.display = is15 ? 'none' : '';
}
});
// кнопка "Изменить" взносы
const editBtn = document.getElementById('insurance-edit-btn');
if (editBtn) editBtn.style.display = is15 ? 'none' : '';
window._taxRate = rate;
}
function updateTaxRateLabels(rate) {
toggleTaxModeUI(rate);
}
// Кнопка открытия модалки профиля — данные загружаются при открытии
const profileModalEl = document.getElementById('profileModal');
if (profileModalEl) {
profileModalEl.addEventListener('show.bs.modal', () => {
loadProfile();
loadBankAccounts();
document.getElementById('profileStatus').innerHTML = '';
});
}
// Сохранение настроек профиля
const profileSaveBtn = document.getElementById('profileSaveBtn');
if (profileSaveBtn) {
profileSaveBtn.addEventListener('click', function() {
const inn = document.getElementById('profileInn').value.trim();
const taxRateEl = document.querySelector('input[name="taxRate"]:checked');
const tax_rate = taxRateEl ? parseInt(taxRateEl.value) : 6;
const status = document.getElementById('profileStatus');
const btn = this;
// Базовая валидация
if (inn && !/^\d{10,12}$/.test(inn)) {
status.innerHTML = '<div class="alert alert-danger py-1 small">ИНН должен содержать 10 или 12 цифр</div>';
return;
}
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Сохранение...';
fetch('api/profile.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
inn: inn,
tax_rate: tax_rate
})
})
.then(r => r.json())
.then(res => {
if (res.success) {
status.innerHTML = '<div class="alert alert-success py-1 small">Настройки сохранены!</div>';
updateTaxRateLabels(tax_rate);
// перезагружаем квартальные итоги с новой ставкой
loadQuarterTotals();
setTimeout(() => {
bootstrap.Modal.getOrCreateInstance(document.getElementById('profileModal')).hide();
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check-lg me-1"></i>Сохранить';
status.innerHTML = '';
}, 600);
} else {
status.innerHTML = '<div class="alert alert-danger py-1 small">' + (res.error || 'Ошибка') + '</div>';
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check-lg me-1"></i>Сохранить';
}
})
.catch(err => {
status.innerHTML = '<div class="alert alert-danger py-1 small">Ошибка соединения</div>';
console.error(err);
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check-lg me-1"></i>Сохранить';
});
});
}
// ==========================================
// Управление банковскими счетами
// ==========================================
function loadBankAccounts() {
fetch('api/bank_accounts.php', { method: 'GET' })
.then(r => r.json())
.then(data => {
const listEl = document.getElementById('bank-accounts-list');
if (!listEl || !data.success) {
console.warn('loadBankAccounts: элемент не найден или ошибка API', data);
return;
}
const accounts = data.accounts || [];
if (accounts.length === 0) {
listEl.innerHTML = '<span class="text-muted small">Нет привязанных счетов</span>';
return;
}
listEl.innerHTML = accounts.map(function(a) {
return '<span class="badge bg-light text-dark border me-2 mb-1" style="font-size:0.85rem;">'
+ a.account_number
+ ' <i class="bi bi-x-circle text-danger ms-1 bank-account-del" data-account="' + a.account_number + '" style="cursor:pointer;" title="Удалить"></i>'
+ '</span>';
}).join('');
// вешаем обработчики удаления
listEl.querySelectorAll('.bank-account-del').forEach(function(icon) {
icon.addEventListener('click', function(e) {
e.stopPropagation();
const acc = this.dataset.account;
document.getElementById('bank-delete-label').textContent = acc;
document.getElementById('bank-delete-status').innerHTML = '';
const confirmBtn = document.getElementById('bankDeleteConfirmBtn');
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i class="bi bi-trash me-1"></i>Удалить';
bootstrap.Modal.getOrCreateInstance(document.getElementById('bankDeleteModal')).show();
});
});
})
.catch(err => console.error('Ошибка загрузки счетов:', err));
}
// Обработчик подтверждения удаления счёта (вешаем один раз)
const bankDeleteConfirmBtn = document.getElementById('bankDeleteConfirmBtn');
if (bankDeleteConfirmBtn) {
bankDeleteConfirmBtn.addEventListener('click', function() {
const status = document.getElementById('bank-delete-status');
const btn = this;
const acc = document.getElementById('bank-delete-label').textContent.trim();
if (!acc) return;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Удаление...';
fetch('api/bank_accounts.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'delete', account_number: acc })
})
.then(r => r.json())
.then(res => {
if (res.success) {
status.innerHTML = '<span class="text-success small">Счёт удалён</span>';
loadBankAccounts();
setTimeout(() => {
bootstrap.Modal.getOrCreateInstance(document.getElementById('bankDeleteModal')).hide();
}, 500);
} else {
status.innerHTML = '<span class="text-danger small">' + (res.error || 'Ошибка') + '</span>';
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-trash me-1"></i>Удалить';
}
})
.catch(err => {
status.innerHTML = '<span class="text-danger small">Ошибка соединения</span>';
console.error(err);
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-trash me-1"></i>Удалить';
});
});
}
// Кнопка добавления счёта
const bankAddBtn = document.getElementById('bank-account-add-btn');
if (bankAddBtn) {
bankAddBtn.addEventListener('click', function() {
const input = document.getElementById('bank-account-input');
const status = document.getElementById('bank-account-status');
const acc = input.value.trim();
if (!acc || acc.length < 10) {
status.innerHTML = '<span class="text-danger small">Введите корректный номер счёта</span>';
return;
}
status.innerHTML = '<span class="text-info small">Добавление...</span>';
fetch('api/bank_accounts.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ account_number: acc })
})
.then(r => r.json())
.then(res => {
if (res.success) {
input.value = '';
status.innerHTML = '<span class="text-success small">Счёт добавлен</span>';
loadBankAccounts();
setTimeout(function() { status.innerHTML = ''; }, 2000);
} else {
status.innerHTML = '<span class="text-danger small">' + (res.error || 'Ошибка') + '</span>';
}
})
.catch(function(err) {
status.innerHTML = '<span class="text-danger small">Ошибка соединения</span>';
console.error(err);
});
});
}
// ==========================================
// Подтверждение email (верификация)
// ==========================================
function showVerifyModal(email) {
document.getElementById('verify-email-label').textContent = email;
document.getElementById('verifyEmailHidden').value = email;
document.getElementById('verifyCodeInput').value = '';
document.getElementById('verifyStatus').innerHTML = '';
const confirmBtn = document.getElementById('verifyConfirmBtn');
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i class="bi bi-check-lg me-1"></i>Подтвердить';
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('verifyModal'));
modal.show();
}
// Подтвердить код
document.getElementById('verifyConfirmBtn').addEventListener('click', function() {
const code = document.getElementById('verifyCodeInput').value.trim();
const email = document.getElementById('verifyEmailHidden').value;
const status = document.getElementById('verifyStatus');
const btn = this;
if (!code || code.length !== 6 || !/^\d{6}$/.test(code)) {
status.innerHTML = '<div class="alert alert-danger py-1 small">Введите 6-значный код</div>';
return;
}
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Проверка...';
fetch('verify.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: code, email: email })
})
.then(r => r.json())
.then(res => {
if (res.success) {
status.innerHTML = '<div class="alert alert-success py-1 small">Email подтверждён!</div>';
setTimeout(() => {
bootstrap.Modal.getOrCreateInstance(document.getElementById('verifyModal')).hide();
// Показываем окно входа
document.getElementById('loginEmail').value = email;
bootstrap.Modal.getOrCreateInstance(document.getElementById('loginModal')).show();
}, 800);
} else {
status.innerHTML = '<div class="alert alert-danger py-1 small">' + (res.error || 'Неверный код') + '</div>';
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check-lg me-1"></i>Подтвердить';
}
})
.catch(err => {
status.innerHTML = '<div class="alert alert-danger py-1 small">Ошибка соединения</div>';
console.error(err);
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check-lg me-1"></i>Подтвердить';
});
});
// Отправить код повторно
document.getElementById('verifyResendBtn').addEventListener('click', function() {
const email = document.getElementById('verifyEmailHidden').value;
const status = document.getElementById('verifyStatus');
const btn = this;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Отправка...';
fetch('login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'resend_code', email: email })
})
.then(r => r.json())
.then(res => {
if (res.success) {
if (res.debug_code) {
document.getElementById('verifyCodeInput').value = res.debug_code;
let errMsg = '<div class="alert alert-danger py-1 small">';
errMsg += '<b>Письмо не отправлено!</b><br>';
errMsg += 'Ошибка: ' + (res.mail_error || 'неизвестна') + '<br>';
if (res.mail_debug && res.mail_debug.length > 0) {
errMsg += '<details class="mt-1"><summary>Лог SMTP</summary>';
res.mail_debug.forEach(function(d) { errMsg += d + '<br>'; });
errMsg += '</details>';
}
errMsg += 'Код: <b style="font-size:1.2em;">' + res.debug_code + '</b></div>';
status.innerHTML = errMsg;
} else {
status.innerHTML = '<div class="alert alert-success py-1 small">Новый код отправлен!</div>';
}
} else {
status.innerHTML = '<div class="alert alert-danger py-1 small">' + (res.error || 'Ошибка') + '</div>';
}
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-arrow-repeat me-1"></i>Отправить код повторно';
})
.catch(err => {
status.innerHTML = '<div class="alert alert-danger py-1 small">Ошибка соединения</div>';
console.error(err);
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-arrow-repeat me-1"></i>Отправить код повторно';
});
});
// Инициализация
checkSession();
</script>
<footer class="text-center text-muted pb-3" style="font-size:0.75rem;">
Разработка © ИП Клюев В.В., ИНН: 223801342200
</footer>
</body>
</html>
Sitemap
Кол-во: 0
XML-карта сайта для поисковиков
?
Sitemap.xml помогает поисковику быстрее находить и индексировать страницы. Особенно важен для крупных сайтов и новых страниц, на которые ещё нет входящих ссылок.
Robots.txt не содержит ссылку на карту сайта. Рекомендуется добавить карту сайта и указать ссылку на нее в robots.txt.
Внутренние ссылки
Кол-во: 4
Ссылки на другие страницы своего сайта
?
Внутренние ссылки распределяют ссылочный вес между страницами и помогают поисковику обходить сайт. Пустые анкоры и ссылки на запрещённые robots.txt страницы — типичные ошибки.
Внутренних ссылок на странице 4 оптимально.
Внутренние ссылки не запрещены к индексации в robots.txt.
Показать внутренние ссылки
| Url | Анкор | Состояние | Анализировать |
|---|---|---|---|
| /tax-editor.php?quarter=1 |
Изменить
|
|
Анализировать url |
| /tax-editor.php?quarter=2 |
Изменить
|
|
Анализировать url |
| /tax-editor.php?quarter=3 |
Изменить
|
|
Анализировать url |
| /tax-editor.php?quarter=4 |
Изменить
|
|
Анализировать url |
Внешние ссылки
Не найдено
Ссылки на сторонние сайты
?
Исходящие внешние ссылки передают часть ссылочного веса на чужие сайты. Ссылки на авторитетные ресурсы безопасны; ссылки на мусорные сайты могут навредить репутации страницы.
Внешних ссылок на странице не найдено.
Конкуренты Готовность: 0%
Конкуренты в Яндексе
Кол-во: 0
Топ сайтов-конкурентов в Яндексе
?
Сайты, чаще всего появляющиеся в ТОПе Яндекса по запросам из семантического ядра этой страницы.
Мы не нашли у вас конкурентов в Яндексе. Сайт или очень молодой или плохо продвигается.
Конкурентов в ТОП-10 Яндекса не нашлось.
Конкуренты в Google
Кол-во: 0
Топ сайтов-конкурентов в Google
?
Сайты, чаще всего появляющиеся в ТОПе Google по запросам из семантического ядра этой страницы.
Конкуренты в Google тоже не найдены. Займитесь продвижением сайта!
Конкурентов в ТОП-10 Google не нашлось.
ФЗ-152: ПД Готовность: 45%
РКН
ОК
Сайт не заблокирован
ИНН
Есть
ИНН найден
Контакты
https://ip-shka.ru/contacts/
Страница с контактами найдена
Политика ПД
https://ip-shka.ru/policy/
Страница политики обработки ПД найдена
Согласие
https://ip-shka.ru/personal/
Страница согласия на обработку ПД найдена
Чекбокс согласия
Не найден
Чекбокс согласия на обработку ПД в формах сайта не найден
Счётчики до согласия
ОК
Счётчики аналитики не собирают данные до согласия пользователя
ЗоЗПП: права потребителей Готовность: 100%
Нарушения
Не выявлены
Признаков дистанционной продажи товаров (интернет-магазина) не обнаружено — требования ЗоЗПП о раскрытии информации продавца к сайту не применяются. Нарушений нет.
ФЗ-149: рекомендательные технологии Готовность: 100%
Нарушения
Не выявлены
Рекомендательные блоки («с этим покупают», «похожие товары» и т.п.) на сайте не обнаружены — требования ст. 10.7 ФЗ-149 к сайту не применяются. Нарушений нет.
ФЗ-38: реклама Готовность: 100%
Нарушения
Не выявлены
Рекламных тематик с обязательными оговорками (медицина, БАД, кредиты и займы, новостройки) на сайте не обнаружено. Нарушений нет.
ФЗ-436: защита детей Готовность: 100%
Нарушения
Не выявлены
Признаков информационной продукции (новости, видео, книги, игры, курсы) не обнаружено — обязательная возрастная маркировка по ФЗ-436 сайту не требуется. Нарушений нет.
Вердикт
Таким сайт ip-shka.ru видят поисковые системы. Процент оптимизации невысокий - 62%. Чтобы улучшить сайт и попасть в ТОП необходимо:
Исправьте ошибки в мета-тегах.
Исправьте ошибки оптимизации.
Поделитесь с друзьями: