Помоги мне исправить код. Я бы хотел, чтобы под каждую открытую таблицу в новой вкладке у нас создавался html, а не открывалась страница как сейчас, в виде попапов.

Вот код. Файл report.ejs:

Навигация
<% auditSections.forEach(function(section, sectionIndex) { %>
<%= section.section %> ▼
<% section.subsections.forEach(function(subsection, subsectionIndex) { %>
<%= subsection.title %>
<% }) %>
<% }) %>
Отчет по SEO Аудиту
Дата создания отчета: <%= reportDate %>

Общее количество ошибок
Технический аудит: <%= totalTechnicalErrors %>

SEO-аудит: <%= totalSeoErrors %>

Поиск по задачам...

Критичность по умолчанию
 
Сортировка по умолчанию
<% auditSections.forEach(function(section, sectionIndex) { %>
<%= section.section %>
<% section.subsections.forEach(function(subsection, subsectionIndex) { %>
<%= subsection.title %>
<% subsection.tasks.forEach(function(task, taskIndex) { %>
<%= task.description %> <% if(task.customErrorText) { %>
<%= task.customErrorText %>
<% } else { %>
Ошибки: <%= task.errorCount %>
<% } %>
<% if(task.showCriticality) { %>
Критичность: <%= task.criticality %>
<% } %>
▼
<% if(task.showDetails){ %>
<%= task.details %>

<% } %> <% if(task.showWhoCanFix){ %>
Кто может исправить: <%= task.whoCanFix %>

<% } %> <% if(task.showHowToFix){ %>
Как исправить:

<% task.howToFix.forEach(function(fix) { %>
<%= fix %>
<% }) %>
<% } %> <% if (task.hasTable || task.fileType==='.html' ) { %> <% if (task.fileType==='.xlsx' ) { %> Результаты ➔ Документ отчёта в папке "tables / <%= task.description %>" <% } else if (task.fileType==='.html' ) { %> Результаты ➔ Документ отчёта в папке "tables / <%= task.description %>" <% } %> <% } %>
<% }) %>
<% }) %>
<% }) %>
Файл index.js:
import xlsx from "xlsx";
import ejs from "ejs";
import fs from "fs";
import path from "path";

// Импорт подпунктов Технический аудит
import information from "./audits/tech/information.js";
import javascript from "./audits/tech/javascript.js";
import url from "./audits/tech/url.js";
import pagination from "./audits/tech/pagination.js";
import hreflang from "./audits/tech/hreflang.js";
import security from "./audits/tech/security.js";
import errors_server from "./audits/tech/errors_server.js";
import resource from "./audits/tech/resource.js";
import robot_txt from "./audits/tech/robot_txt.js";
import check_sitemap from "./audits/tech/check_sitemap.js";
import errors_html from "./audits/tech/errors_html.js";
import images from "./audits/tech/images.js";
import optimize_html_css_js from "./audits/tech/optimize_html_css_js.js";
import redirects from "./audits/tech/redirects.js";
import accessibility_analysis from "./audits/tech/accessibility_analysis.js";
import domen from "./audits/tech/domen.js";
import hosting from "./audits/tech/hosting.js";

// Импорт подпунктов SEO-аудит
import indexing from "./audits/seo/indexing_site.js";
import seo_mistakes from "./audits/seo/seo_mistakes.js";
import content from "./audits/seo/content.js";
import other_seo_mistakes from "./audits/seo/other_seo_mistakes.js";
import links from "./audits/seo/links.js";

// Импорт позиций сайта
import position from "./audits/positions/position.js";

// Функция для проверки существования файла
const fileExists = (filePath) => fs.existsSync(filePath);

// Функция для получения строк Excel
function getRowCountFromExcel(filePath) {
if (!fileExists(filePath)) return 0;
const workbook = xlsx.readFile(filePath);
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const rows = xlsx.utils.sheet_to_json(worksheet, { header: 1 });
return rows.length - 1; // Возвращаем количество строк, минус заголовок
}

// Функция для получения заголовков Excel
function getExcelHeaders(filePath) {
if (!fileExists(filePath)) return [];
const workbook = xlsx.readFile(filePath);
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const rows = xlsx.utils.sheet_to_json(worksheet, { header: 1 });
return rows[0] || []; // Возвращаем заголовки
}

// Функция для получения строк Excel с заполнением пустых ячеек
function getExcelRows(filePath) {
if (!fileExists(filePath)) return [];
const workbook = xlsx.readFile(filePath);
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const rows = xlsx.utils.sheet_to_json(worksheet, { header: 1 });
if (rows.length === 0) return [];
const headers = rows[0];
const dataRows = rows.slice(1);
return dataRows.map(row => {
return headers.map((_, index) => (row[index] !== undefined ? row[index] : ""));
});
}

// Массив с секциями и подпунктами
const auditSections = [
{
section: "Технический аудит",
subsections: [
information,
javascript,
url,
pagination,
hreflang,
security,
errors_server,
resource,
robot_txt,
check_sitemap,
errors_html,
images,
optimize_html_css_js,
redirects,
accessibility_analysis,
domen,
hosting
]
},
{
section: "SEO-аудит",
subsections: [
indexing,
seo_mistakes,
content,
other_seo_mistakes,
links
]
},
{
section: "Позиции сайта",
subsections: [
position
]
}
];

// Возможные цвета для ошибок
const allowedColors = ["green", "red", "orange"];

// Инициализация счетчиков ошибок по разделам
let totalTechnicalErrors = 0;
let totalSeoErrors = 0;

// Проходим по каждому подпункту и задаче, обрабатывая данные
auditSections.forEach(section => {
section.subsections.forEach(subsection => {
subsection.tasks.forEach(task => {
const hasFile = fileExists(task.filePath);
if (hasFile) {
task.fileType = path.extname(task.filePath).toLowerCase();
// Определяем тип файла
if (task.fileType === ".html") {
// Формируем относительный путь для открытия в новой вкладке
task.reportUrl = path.relative(path.dirname("seo-report.html"), task.filePath).replace(/\/g, "/");
}
} else {
task.fileType = null;
}

// Обработка кастомных ошибок if (!task.customErrorText) { const errorCount = hasFile && task.fileType === ".xlsx" ? getRowCountFromExcel(task.filePath) : 0; task.errorCount = errorCount; task.errorCountClass = errorCount === 0 ? "error-low" : "error-high"; // Добавление к общему количеству ошибок if (section.section === "Технический аудит") { totalTechnicalErrors += errorCount; } else if (section.section === "SEO-аудит") { totalSeoErrors += errorCount; } } else { // Проверка на допустимый цвет, установка дефолта при необходимости const color = allowedColors.includes(task.errorBackgroundColor) ? task.errorBackgroundColor : "red"; task.errorCountClass = `error-${color}`; // Добавление к общему количеству ошибок, если цвет не зеленый if (color !== "green") { const errorNumber = parseInt(task.customErrorText) || 0; if (section.section === "Технический аудит") { totalTechnicalErrors += errorNumber; } else if (section.section === "SEO-аудит") { totalSeoErrors += errorNumber; } } } // Класс критичности if (task.customErrorText && task.errorBackgroundColor === "green") { task.showCriticality = false; } else { task.showCriticality = task.showCriticality !== undefined ? task.showCriticality : true; } task.criticalityClass = task.criticality === "Высокая" ? "criticality-high" : task.criticality === "Средняя" ? "criticality-medium" : "criticality-low"; task.hasTable = hasFile && task.fileType === ".xlsx"; task.tableHeaders = task.hasTable ? getExcelHeaders(task.filePath) : []; task.tableRows = task.hasTable ? getExcelRows(task.filePath) : []; });
});
});

// Получение текущей даты для отчета
const reportDate = new Date().toLocaleDateString("ru-RU");

// Генерация HTML отчета
ejs.renderFile("templates/report.ejs", {
auditSections,
totalTechnicalErrors,
totalSeoErrors,
reportDate
}, (err, html) => {
if (err) {
console.error(err);
return;
}

// Сохранение HTML отчета
fs.writeFileSync("seo-report.html", html);
console.log("Отчет успешно создан!");
});

Файл scripts.ts:
/**

TODO: Нужна типизация!
*/
// Переключение отображения деталей задачи и стрелки
function toggleDetails(id: string): void {
const element: HTMLElement | null = document.getElementById(id);
const arrowId: string = id.replace("desc_", "arrow_");
const arrow: HTMLElement | null = document.getElementById(arrowId);

// Проверка наличия необходимых элементов
if (!element || !arrow) {
console.warn(Element with id ${id} or arrow with id ${arrowId} not found.);
return;
}

// Переключение состояния отображения
if (element.style.display === "none" || element.style.display === "") {
element.style.display = "block";
arrow.classList.add("up");
arrow.textContent = "▲";
} else {
element.style.display = "none";
arrow.classList.remove("up");
arrow.textContent = "▼";
}
}

// Переключение отображения таблицы результатов
function toggleTable(id: string): void {
const table: HTMLElement | null = document.getElementById(id);

// Проверка наличия таблицы
if (!table) {
console.warn(Table with id ${id} not found.);
return;
}

// Переключение состояния отображения
table.style.display = (table.style.display === "none" || table.style.display === "") ? "block" : "none";
}

// Функция для открытия таблицы в новой вкладке
function openTableInNewTab(id: string, title: string): void {
const tableDiv = document.getElementById(id);
if (!tableDiv) {
console.warn(Таблица с id ${id} не найдена.);
return;
}

const tableHtml = tableDiv.innerHTML;

// Получение текущих стилей для таблицы
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"], style')).map(styleEl => {
if (styleEl instanceof HTMLLinkElement) {
// Проверяем, является ли ссылка относительной или абсолютной
const href = styleEl.href.startsWith('http') ? styleEl.href : ${window.location.origin}${styleEl.getAttribute('href')};
return <link rel="stylesheet" href="${href}">;
} else if (styleEl instanceof HTMLStyleElement) {
return <style>${styleEl.innerHTML}</style>;
}
return '';
}).join('\n');

// Создание полноценной HTML-страницы
const htmlContent = <!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Результаты - ${title}</title> ${styles} <style> /* Дополнительные стили для корректного отображения таблицы */ body { padding: 20px; font-family: Arial, sans-serif; } table { width: 100%; border-collapse: collapse; margin-top: 20px; } th, td { border: 1px solid #ddd; padding: 8px; } th { background-color: #f2f2f2; text-align: left; } </style> </head> <body> <h2>Результаты: ${title}</h2> ${tableHtml} </body> </html> ;

// Открытие новой вкладки и запись содержимого
const newWindow = window.open('', '_blank');
if (newWindow) {
newWindow.document.open();
newWindow.document.write(htmlContent);
newWindow.document.close();
} else {
console.warn('Не удалось открыть новую вкладку.');
}
}

// Функция для фильтрации задач
function filterTasks(): void {
const searchInputElem = document.getElementById("searchInput") as HTMLInputElement;
const filterSelectElem = document.getElementById("filterSelect") as HTMLSelectElement;
if (!searchInputElem || !filterSelectElem) {
console.warn("Search input or filter select element not found.");
return;
}

const searchInput: string = searchInputElem.value.toLowerCase();
const filterSelect: string = filterSelectElem.value;

const auditTasks = document.getElementsByClassName("audit-task");
const auditSections = document.getElementsByClassName("audit-section");
const auditSubsections = document.getElementsByClassName("audit-subsection");

// Фильтрация задач
if (auditTasks) {
Array.from(auditTasks).forEach((task) => {
const taskElement = task as HTMLElement;
const descriptionElem = taskElement.querySelector(".audit-header > div");
const description = descriptionElem?.textContent?.toLowerCase() ?? "";
const criticality = taskElement.getAttribute("data-criticality");
const matchesSearch = description.includes(searchInput);
const matchesFilter = filterSelect === "" || criticality === filterSelect;
taskElement.style.display = (matchesSearch && matchesFilter) ? "" : "none";
});
}

// Скрытие разделов без видимых задач
Array.from(auditSections).forEach((section) => {
const sectionElement = section as HTMLElement;
const visibleTasksInSection = sectionElement.querySelectorAll('.audit-task:not([style*="display: none"])').length;
sectionElement.style.display = (visibleTasksInSection === 0) ? "none" : "";
});

// Скрытие подпунктов без видимых задач
Array.from(auditSubsections).forEach((subsection) => {
const subsectionElement = subsection as HTMLElement;
const visibleTasksInSubsection = subsectionElement.querySelectorAll('.audit-task:not([style*="display: none"])').length;
subsectionElement.style.display = (visibleTasksInSubsection === 0) ? "none" : "";
});

// Сброс сортировки при фильтрации
const sortSelectElem = document.getElementById("sortSelect") as HTMLSelectElement;
if (sortSelectElem) {
sortSelectElem.value = "";
resetSort();
}
}

// Функция для сортировки задач
function sortTasks(): void {
const sortSelect = document.getElementById("sortSelect") as HTMLSelectElement | null;
if (!sortSelect) {
console.error("Элемент sortSelect не найден");
return;
}

const sortOption = sortSelect.value;
const auditSections = document.getElementsByClassName("audit-section");

if (sortOption === "") {
resetSort();
return;
}

for (let i = 0; i < auditSections.length; i++) {
const section = auditSections[i] as HTMLElement;
const subsections = section.getElementsByClassName("audit-subsection");
for (let j = 0; j < subsections.length; j++) {
const subsection = subsections[j] as HTMLElement;
let tasks = Array.from(subsection.getElementsByClassName("audit-task")) as HTMLElement[];

tasks.sort((a, b) => { const errorA = parseInt(a.getAttribute("data-error-count") || "0", 10); const errorB = parseInt(b.getAttribute("data-error-count") || "0", 10); if (sortOption === "errorAsc") { return errorA - errorB; } else if (sortOption === "errorDesc") { return errorB - errorA; } return 0; }); // Перестановка задач в DOM согласно сортировке tasks.forEach(task => subsection.appendChild(task)); }
}
}

// Функция для переключения подменю в навигации
function toggleSubsections(sectionIndex: number): void {
const subsectionList = document.getElementById(subsections-${sectionIndex});
if (!subsectionList) {
console.error(Подраздел subsections-${sectionIndex} не найден);
return;
}

const mainLink = subsectionList.previousElementSibling as HTMLElement | null;
if (!mainLink) {
console.error(Предыдущий элемент для subsections-${sectionIndex} не найден);
return;
}

if (subsectionList.style.display === "none" || subsectionList.style.display === "") {
subsectionList.style.display = "block";
mainLink.classList.remove("collapsed");
} else {
subsectionList.style.display = "none";
mainLink.classList.add("collapsed");
}
}

// Функция для подсветки активного пункта меню при клике и выполнения скролла
function highlightMenuItem(event: MouseEvent): void {
const menuLinks = document.querySelectorAll('.sidebar a[href^="#"]');
menuLinks.forEach(link => link.classList.remove("active"));

const target = event.currentTarget as HTMLElement;
target.classList.add("active");
}

// Функция для переключения стрелочки в критичности
function toggleCriticality(event: MouseEvent, id: string): void {
event.stopPropagation(); // Предотвращает всплытие события до audit-header

const criticalityElement = document.getElementById(id);
if (!criticalityElement) {
console.error(Элемент с id ${id} не найден);
return;
}

criticalityElement.classList.toggle("open");
}

// Функция для сброса сортировки до исходного порядка
function resetSort(): void {
const auditSections = document.getElementsByClassName("audit-section");
for (let i = 0; i < auditSections.length; i++) {
const section = auditSections[i] as HTMLElement;
const subsections = section.getElementsByClassName("audit-subsection");
for (let j = 0; j < subsections.length; j++) {
const subsection = subsections[j] as HTMLElement;
const tasks = Array.from(subsection.getElementsByClassName("audit-task")) as HTMLElement[];
tasks.sort((a, b) => {
const orderA = parseInt(a.getAttribute("data-original-order") || "0", 10);
const orderB = parseInt(b.getAttribute("data-original-order") || "0", 10);
return orderA - orderB;
});

// Перестановка задач в DOM согласно исходному порядку tasks.forEach(task => subsection.appendChild(task)); }
}
}

// Инициализация элементов при загрузке страницы
document.addEventListener("DOMContentLoaded", () => {
const menuLinks = document.querySelectorAll('.sidebar a[href^="#"]');
menuLinks.forEach(link => {
link.addEventListener("click", function (event: MouseEvent): void {
event.preventDefault(); // Предотвращаем стандартное поведение ссылки

// Активируем ссылку и скроллим к соответствующему разделу highlightMenuItem(event); const targetId = this.getAttribute("href")!.substring(1); const targetElement = document.getElementById(targetId); if (targetElement) { targetElement.scrollIntoView({ behavior: "smooth" }); } }); // Добавляем подсветку активного пункта меню при наведении link.addEventListener("click", highlightMenuItem);
});

// По умолчанию раскрываем все подпункты
const allSubsections = document.querySelectorAll(".subsections");
allSubsections.forEach(subsection => {
subsection.style.display = "block";
});

// Устанавливаем иконки для раскрытых подпунктов
const allMainLinks = document.querySelectorAll(".main-section > a");
allMainLinks.forEach(link => {
link.classList.remove("collapsed");
});
});