Отзывы и предложения к софту от AleXStam
Поговорим о...
// Функция загрузки данных скважины из мульти-файла
async function parseWellDataFromMulti(wellId, signal = null) {
// ПРОВЕРЯЕМ отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

if (!currentMultiWellData || !currentMultiWellData.wells) {
throw new Error('Данные мульти-файла не загружены');
}

// ПРОВЕРЯЕМ КЕШ с защитой от ошибок
const cachedData = getCachedWellData(wellId);
if (cachedData) {
console.log('Используем кешированные данные для скважины:', wellId);

// ПРОВЕРЯЕМ отмену операции перед возвратом данных
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

xmlData = cachedData;
return;
}

showLoading('Загрузка данных скважины...');

try {
updateProgress(10, 'Поиск информации о скважине...');

// ПРОВЕРЯЕМ отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

const targetWellMeta = currentMultiWellData.wells.find(w => w.wellId === wellId);
if (!targetWellMeta) {
throw new Error(`Скважина с ID ${wellId} не найдена в списке`);
}

let xmlContent = '';
let sourceFile = '';

// Случай 1: ZIP архив (данные в fileCache)
if (targetWellMeta.sourceFile) {
sourceFile = targetWellMeta.sourceFile;
updateProgress(30, 'Загрузка данных из кеша...');

// ПРОВЕРЯЕМ отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

xmlContent = fileCache.get(sourceFile);
if (!xmlContent) {
throw new Error(`Файл ${sourceFile} не найден в кеше`);
}
}
// Случай 2: Отдельный INU файл (данные в currentMultiWellData.xmlContent)
else if (currentMultiWellData.xmlContent) {
updateProgress(30, 'Загрузка данных из текущего файла...');
xmlContent = currentMultiWellData.xmlContent;
sourceFile = 'current_inu_file';
}
// Случай 3: Не удалось определить источник
else {
throw new Error('Не удалось определить источник данных для скважины');
}

updateProgress(50, 'Быстрая обработка всех данных...');

// ПРОВЕРЯЕМ отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

// ИСПОЛЬЗУЕМ БЫСТРЫЙ МЕТОД ДЛЯ ВСЕХ ДАННЫХ
console.time(`Parse ALL data for ${wellId}`);
const allWellData = await parseAllWellDataFast(xmlContent, wellId);
console.timeEnd(`Parse ALL data for ${wellId}`);

// ПРОВЕРЯЕМ отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

if (allWellData) {
// ВАЖНО: Сохраняем wellId в данных для корректного отображения
allWellData.wellId = wellId;

xmlData = allWellData;

// Сохраняем в кеш с защитой от ошибок
setCachedWellData(wellId, xmlData);

updateProgress(100, 'Готово!');

// ПРОВЕРЯЕМ отмену операции перед завершением
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

setTimeout(() => {
hideLoading();
}, 50);
return;
}

throw new Error('Не удалось извлечь данные скважины');

} catch (error) {
// Игнорируем ошибки отмены
if (error.name === 'AbortError') {
throw error;
}

hideLoading();
console.error('Ошибка загрузки данных скважины:', error);

// Очищаем кеш для этой скважины при ошибке
try {
wellDataCache.delete(wellId);
} catch (e) {
console.error('Ошибка очистки кеша:', e);
}

throw error;
}
}

// Вспомогательные функции для кеша
function getCachedWellData(wellId) {
try {
return wellDataCache.get(wellId);
} catch (error) {
console.error('Ошибка доступа к кешу данных:', error);
return null;
}
}

function setCachedWellData(wellId, data) {
try {
wellDataCache.set(wellId, data);
} catch (error) {
console.error('Ошибка сохранения в кеш:', error);
}
}
Понял! Нужно доработать логику для файлов 906, 911 и других, чтобы они тоже обрабатывались как мульти-файлы при наличии нескольких скважин. Вот полная реализация:

1. Обновленная функция parseSingleWellData с определением мульти-скважинности

```javascript
// Функция для парсинга одиночных файлов с поддержкой мульти-скважин
async function parseSingleWellData(xmlDoc, fileType, fileName = null) {
try {
// Считаем количество уникальных скважин в файле
const wellPoints = xmlDoc.getElementsByTagName('MR_DBA.POINT_OBSERV_POAA');
const uniqueWellIds = new Set();
const wellsMetadata = [];

for (let i = 0; i < wellPoints.length; i++) {
const wellId = getSafeTextContent(wellPoints[i], 'ID_POINT_OBSERV_POAA');
if (wellId && wellId !== 'Не указано') {
uniqueWellIds.add(wellId);

// Собираем метаданные для списка скважин
const wellName = getSafeTextContent(wellPoints[i], 'NAME_FULL_POAA') || 'Без названия';
const object = getSafeTextContent(wellPoints[i], 'ID_OBJECT_WORK_POAA') || 'Не указан';
const description = getSafeTextContent(wellPoints[i], 'DESCRIPTION_POAA') || '';

// Получаем номер линии для этой скважины
const lineId = getSafeTextContent(wellPoints[i], 'ID_LINE_COMMON_POAA');
let lineNumber = 'Не указано';
if (lineId) {
const lineCommon = xmlDoc.getElementsByTagName('MR_DBA.LINE_COMMON_LNCM');
for (let j = 0; j < lineCommon.length; j++) {
const line = lineCommon[j];
const currentLineId = getSafeTextContent(line, 'ID_LINE_COMMON_LNCM');
if (currentLineId === lineId) {
lineNumber = getSafeTextContent(line, 'NAME_FULL_LNCM') || 'Не указано';
break;
}
}
}

// Форматируем отображаемое название
const displayName = formatWellDisplayName(wellName, lineNumber);

wellsMetadata.push({
wellId: wellId,
wellName: wellName,
displayName: displayName,
object: object,
description: description,
lineNumber: lineNumber
});
}
}

const wellCount = uniqueWellIds.size;
console.log(`Найдено уникальных скважин в файле ${fileName}: ${wellCount}`);

// Если скважин больше одной, обрабатываем как мульти-файл
if (wellCount > 1 && (fileType === 'all' || fileType === 'final' || fileType === 'primary' || fileType === 'unknown')) {
console.log(`Файл ${fileType} (${fileName}) содержит ${wellCount} скважин, обрабатываем как мульти-файл`);

if (wellsMetadata.length > 0) {
return {
isMulti: true,
wells: wellsMetadata,
wellCount: wellCount
};
}
}

// Если скважина одна или это специальный тип файла - парсим как одиночную
console.log(`Обработка файла ${fileName} как одиночной скважины`);

// Извлекаем данные о скважине (берем первую найденную)
const pointObserv = wellPoints[0];
if (!pointObserv) {
throw new Error('Не найдены данные о скважине в файле');
}

const objectWork = xmlDoc.getElementsByTagName('MR_DBA.OBJECT_WORK_OBWA')[0];
const ground = xmlDoc.getElementsByTagName('MR_DBA.GROUND_GRNA')[0];
const pointObservType = xmlDoc.getElementsByTagName('MR_DBA.POINT_OBSERV_TYPE_PNOT')[0];
const lineCommon = xmlDoc.getElementsByTagName('MR_DBA.LINE_COMMON_LNCM')[0];

// Основная информация о скважине
const wellName = getSafeTextContent(pointObserv, 'NAME_FULL_POAA');
const object = getSafeTextContent(objectWork, 'NAME_FULL_OBWA');
const area = getSafeTextContent(ground, 'NAME_FULL_GRNA');
const wellType = getSafeTextContent(pointObservType, 'NAME_SHORT_PNOT');
const lineNumber = getSafeTextContent(lineCommon, 'NAME_FULL_LNCM');
const wellDescription = getSafeTextContent(pointObserv, 'DESCRIPTION_POAA');

// Даты бурения и бригада
let drillingStartDate = '';
let drillingEndDate = '';
let drillingBrigade = 'Не указано';

drillingStartDate = formatDate(getSafeTextContent(pointObserv, 'DATE_DRIFTING_BEG_POAA'));
drillingEndDate = formatDate(getSafeTextContent(pointObserv, 'DATE_DRIFTING_END_POAA'));

// Получаем название бригады
const brigadeId = getSafeTextContent(pointObserv, 'BRIGADE_POAA');
if (brigadeId && brigadeId !== 'Не указано') {
const divisionNodes = xmlDoc.getElementsByTagName('MR_DBA.OUR_DIVISION');
for (let i = 0; i < divisionNodes.length; i++) {
const division = divisionNodes[i];
const divisionId = getSafeTextContent(division, 'ID_CONTRACTOR');
if (divisionId === brigadeId) {
const brigadeName = getSafeTextContent(division, 'NAME_SHORT');
if (brigadeName && brigadeName !== 'Не указано') {
drillingBrigade = brigadeName;
break;
}
}
}

if (drillingBrigade === 'Не указано') {
drillingBrigade = `Бригада ${brigadeId}`;
}
}

// Создаем карту сотрудников
const employeeMap = new Map();
const employeeNodes = xmlDoc.getElementsByTagName('MR_DBA.OUR_EMPLOYEE');
for (let i = 0; i < employeeNodes.length; i++) {
const employee = employeeNodes[i];
const employeeId = getSafeTextContent(employee, 'ID_CONTRACTOR');
const employeeName = getSafeTextContent(employee, 'NAME_SHORT');
if (employeeId && employeeName && employeeId !== 'Не указано' && employeeName !== 'Не указано') {
employeeMap.set(employeeId, employeeName);
}
}

// Данные документирования
let primaryDocInfo = null;
let finalDocInfo = null;
let gisDocInfo = null;

const docNodes = xmlDoc.getElementsByTagName('MR_DBA.PO_DOCUM_PODC');
for (let i = 0; i < docNodes.length; i++) {
const doc = docNodes[i];
const docType = getSafeTextContent(doc, 'ID_TYPE_DOCUM_PODC');
const startDate = formatDate(getSafeTextContent(doc, 'DATE_BEG_DOCUM_PODC'));
const endDate = formatDate(getSafeTextContent(doc, 'DATE_END_DOCUM_PODC'));
const depth = formatNumber(getSafeTextContent(doc, 'DEPTH_FACT_PODC'));
const authorId = getSafeTextContent(doc, 'AUTHOR_PODC');
let authorName = 'Не указано';

if (authorId && authorId !== 'Не указано' && employeeMap.has(authorId)) {
authorName = employeeMap.get(authorId);
} else if (authorId && authorId !== 'Не указано') {
authorName = `Код: ${authorId}`;
}

const docInfo = {
startDate,
endDate,
depth,
author: authorName
};

if (docType.includes('Первич') || docType.includes('ПЕРВИЧ') || docType === '1') {
primaryDocInfo = docInfo;
} else if (docType.includes('Итогов') || docType.includes('ИТОГОВ') || docType === '2') {
finalDocInfo = docInfo;
} else if (docType.includes('ГИС') || docType.includes('геофизик')) {
gisDocInfo = docInfo;
}
}

// Создаем карту типов опробования
const assayTypeMap = new Map();
const assayTypeNodes = xmlDoc.getElementsByTagName('MR_DBA.ASSAY_TYPE_PROP_ASTR');
for (let i = 0; i < assayTypeNodes.length; i++) {
const assayType = assayTypeNodes[i];
const id = getSafeTextContent(assayType, 'ID_ASSAY_TYPE_PROP_ASTR');
const name = getSafeTextContent(assayType, 'NAME_FULL_ASTR');
if (id && name && id !== 'Не указано' && name !== 'Не указано') {
assayTypeMap.set(id, name);
}
}

// Создаем карту типов документирования для интервалов
const docTypeMap = new Map();
const docNodesForIntervals = xmlDoc.getElementsByTagName('MR_DBA.PO_DOCUM_PODC');
for (let i = 0; i < docNodesForIntervals.length; i++) {
const doc = docNodesForIntervals[i];
const docId = getSafeTextContent(doc, 'ID_PO_DOCUM_PODC');
const docType = getSafeTextContent(doc, 'ID_TYPE_DOCUM_PODC');
if (docId && docType && docId !== 'Не указано' && docType !== 'Не указано') {
docTypeMap.set(docId, docType);
}
}

// Создаем карту классификаторов
const classifierMap = new Map();
const classifierNodes = xmlDoc.getElementsByTagName('MR_DBA.CLASSIFIER');
for (let i = 0; i < classifierNodes.length; i++) {
const classifier = classifierNodes[i];
const nodeId = getSafeTextContent(classifier, 'ID_NODE_ID');
const nodeName = getSafeTextContent(classifier, 'NODE_NAME');
if (nodeId && nodeName && nodeId !== 'Не указано' && nodeName !== 'Не указано') {
classifierMap.set(nodeId, nodeName);
}
}

// Создаем карту единиц измерения
const measureMap = new Map();
const measureNodes = xmlDoc.getElementsByTagName('MR_DBA.MEASURE');
for (let i = 0; i < measureNodes.length; i++) {
const measure = measureNodes[i];
const measureId = getSafeTextContent(measure, 'ID_MEASURE');
const measureName = getSafeTextContent(measure, 'NAME_SHORT');
if (measureId && measureName && measureId !== 'Не указано' && measureName !== 'Не указано') {
measureMap.set(measureId, measureName);
}
}

// Извлекаем интервалы документирования для текущей скважины
const currentWellId = getSafeTextContent(pointObserv, 'ID_POINT_OBSERV_POAA');
const primaryIntervals = [];
const finalIntervals = [];
const gisIntervals = [];

// Обрабатываем интервалы документирования
const intervalNodes = xmlDoc.getElementsByTagName('MR_DBA.PO_DOCUM_INT_PODI');
for (let i = 0; i < intervalNodes.length; i++) {
const interval = intervalNodes[i];
const docId = getSafeTextContent(interval, 'ID_PO_DOCUM_PODI');

// Проверяем, что интервал принадлежит текущей скважине
let belongsToCurrentWell = false;
if (docId && docId !== 'Не указано') {
// Ищем документирование для этого интервала
for (let j = 0; j < docNodes.length; j++) {
const doc = docNodes[j];
const currentDocId = getSafeTextContent(doc, 'ID_PO_DOCUM_PODC');
const docWellId = getSafeTextContent(doc, 'ID_POINT_OBSERV_PODC');

if (currentDocId === docId && docWellId === currentWellId) {
belongsToCurrentWell = true;
break;
}
}
}

if (!belongsToCurrentWell) continue;

const from = parseFloat(getSafeTextContent(interval, 'PO_DOCUM_FROM_PODI') || '0');
const to = parseFloat(getSafeTextContent(interval, 'PO_DOCUM_TO_PODI') || '0');
const thickness = parseFloat(getSafeTextContent(interval, 'THICKNESS_PODI') || '0');
const description = getSafeTextContent(interval, 'DESCRIPTION_PODI');
const numberPP = parseInt(getSafeTextContent(interval, 'NUMBER_PP_PODI') || '0');

// Определяем тип документирования
let documentationType = 'Не определено';
if (docId && docId !== 'Не указано' && docTypeMap.has(docId)) {
documentationType = docTypeMap.get(docId);
}

// Получаем литологию
const lithologyId = getSafeTextContent(interval, 'ID_LITHOLOGY_PODI');
let lithology = 'Не указано';
if (lithologyId && classifierMap.has(lithologyId)) {
lithology = classifierMap.get(lithologyId);
}

// Получаем стратиграфию
const stratigraphyId = getSafeTextContent(interval, 'ID_STRATIGRAPHY_PODI');
let stratigraphy = 'Не указано';
if (stratigraphyId && classifierMap.has(stratigraphyId)) {
stratigraphy = convertStratigraphyText(classifierMap.get(stratigraphyId));
}

const intervalData = {
numberPP,
from,
to,
thickness,
stratigraphy,
lithology,
description
};

// Распределяем интервалы по типам документирования
if ((documentationType.includes('Первич') || documentationType.includes('ПЕРВИЧ') || documentationType === '1') &&
!documentationType.includes('ГИС') && !documentationType.includes('геофизик')) {
primaryIntervals.push(intervalData);
} else if ((documentationType.includes('Итогов') || documentationType.includes('ИТОГОВ') || documentationType === '2') &&
!documentationType.includes('ГИС') && !documentationType.includes('геофизик')) {
finalIntervals.push(intervalData);
} else if (documentationType.includes('ГИС') || documentationType.includes('геофизик')) {
gisIntervals.push(intervalData);
} else {
if (fileType === 'all' && !documentationType.includes('ГИС') && !documentationType.includes('геофизик')) {
primaryIntervals.push(intervalData);
finalIntervals.push(intervalData);
} else if (fileType === 'primary' && !documentationType.includes('ГИС') && !documentationType.includes('геофизик')) {
primaryIntervals.push(intervalData);
} else if (fileType === 'final' && !documentationType.includes('ГИС') && !documentationType.includes('геофизик')) {
finalIntervals.push(intervalData);
}
}
}

// СОРТИРУЕМ интервалы по порядковому номеру
primaryIntervals.sort((a, b) => a.numberPP - b.numberPP);
finalIntervals.sort((a, b) => a.numberPP - b.numberPP);
gisIntervals.sort((a, b) => a.numberPP - b.numberPP);

// Извлекаем данные опробования для текущей скважины
const assays = [];
const assayNodes = xmlDoc.getElementsByTagName('MR_DBA.ASSAY_ASSA');
for (let i = 0; i < assayNodes.length; i++) {
const assay = assayNodes[i];
const assayWellId = getSafeTextContent(assay, 'ID_POINT_OBSERV_ASSA');

// Пропускаем пробы других скважин
if (assayWellId !== currentWellId) continue;

const number = parseInt(getSafeTextContent(assay, 'ASSAY_NUMBER_ASSA') || '0');
const from = parseFloat(getSafeTextContent(assay, 'INTERV_FROM_ASSA') || '0');
const to = parseFloat(getSafeTextContent(assay, 'INTERV_TO_ASSA') || '0');
const assayId = getSafeTextContent(assay, 'ID_ASSAY_ASSA');
const description = getSafeTextContent(assay, 'DESCRIPTION_ASSA');

// Определяем тип опробования по ID_ASSAY_TYPE_PROP_ASSA
const assayTypeId = getSafeTextContent(assay, 'ID_ASSAY_TYPE_PROP_ASSA');
let type = 'Неизвестно';
if (assayTypeId && assayTypeMap.has(assayTypeId)) {
type = assayTypeMap.get(assayTypeId);
}

// Определяем тип документирования для пробы
let documentationType = 'Не определено';

// Проверяем историю опробования для определения типа документирования
const assayHistoryNodes = xmlDoc.getElementsByTagName('MR_DBA.ASSAY_HISTORY_ASHI');
for (let j = 0; j < assayHistoryNodes.length; j++) {
const history = assayHistoryNodes[j];
const historyAssayId = getSafeTextContent(history, 'ID_ASSAY_ASHI');

if (historyAssayId === assayId) {
const docTypeId = getSafeTextContent(history, 'ID_TYPE_DOCUM_ASHI');
if (docTypeId && docTypeId !== 'Не указано') {
if (docTypeId.includes('ПЕРВИЧ') || docTypeId.includes('Первич') || docTypeId === '1') {
documentationType = 'Первичное';
} else if (docTypeId.includes('ИТОГОВ') || docTypeId.includes('Итогов') || docTypeId === '2') {
documentationType = 'Итоговое';
}
}
break;
}
}

// Если не нашли в истории, определяем по интервалу
if (documentationType === 'Не определено') {
const assayFrom = from;
const assayTo = to;

let isPrimary = false;
for (const interval of primaryIntervals) {
if (assayFrom >= interval.from && assayTo <= interval.to) {
isPrimary = true;
break;
}
}

let isFinal = false;
for (const interval of finalIntervals) {
if (assayFrom >= interval.from && assayTo <= interval.to) {
isFinal = true;
break;
}
}

if (isPrimary && !isFinal) {
documentationType = 'Первичное';
} else if (isFinal && !isPrimary) {
documentationType = 'Итоговое';
} else if (isPrimary && isFinal) {
documentationType = 'Оба типа';
}
}

// Получаем дополнительные данные опробования
const volume = getSafeTextContent(assay, 'VOLUME_ASSA');
const volumeUnit = getSafeTextContent(assay, 'ID_MEASURE_VOL_ASSA');
const weight = getSafeTextContent(assay, 'WEIGHT_ASSA');
const weightUnit = getSafeTextContent(assay, 'ID_MEASURE_WEIGHT_ASSA');
const barcode = getSafeTextContent(assay, 'ASSAY_NUM_THROUGH_ASSA');
const place = getSafeTextContent(assay, 'ID_ASSAY_PLACE_ASSA');
const authorId = getSafeTextContent(assay, 'AUTHOR_MAIN_ASSA');
const samplingDate = formatDate(getSafeTextContent(assay, 'DATE_SAMPLING_ASSA'));

let authorName = '';
if (authorId && authorId !== 'Не указано' && employeeMap.has(authorId)) {
authorName = employeeMap.get(authorId);
} else if (authorId && authorId !== 'Не указано') {
authorName = `Код: ${authorId}`;
}

let value = '';
let unit = '';

if (volume && volumeUnit && volume !== 'Не указано' && volumeUnit !== 'Не указано') {
value = volume;
if (measureMap.has(volumeUnit)) {
unit = measureMap.get(volumeUnit);
}
} else if (weight && weightUnit && weight !== 'Не указано' && weightUnit !== 'Не указано') {
value = weight;
if (measureMap.has(weightUnit)) {
unit = measureMap.get(weightUnit);
}
}

// Форматируем тип документирования
let formattedDocType = documentationType;
if (documentationType === 'Первичное') {
formattedDocType = 'Первичное документирование';
} else if (documentationType === 'Итоговое') {
formattedDocType = 'Итоговое документирование';
} else if (documentationType === 'Оба типа') {
formattedDocType = 'Оба типа документирования';
}

// Фильтруем пустые описания и места отбора
const assayDescription = (description && description !== 'Не указано') ? description : '';
const assayPlace = (place && place !== 'Не указано' && place.length <= 20) ? place : '';

assays.push({
number,
type,
documentationType: formattedDocType,
from,
to,
value,
unit,
barcode,
place: assayPlace,
author: authorName,
samplingDate,
description: assayDescription
});
}

// Сортируем пробы по номеру
assays.sort((a, b) => a.number - b.number);

// Сохраняем извлеченные данные
const wellData = {
wellName,
object,
area,
wellType,
wellNumber: wellName,
lineNumber,
wellDescription,
drillingStartDate,
drillingEndDate,
drillingBrigade,
primaryDocInfo,
finalDocInfo,
gisDocInfo,
primaryDocumentation: primaryIntervals,
finalDocumentation: finalIntervals,
gisDocumentation: gisIntervals,
assays,
coreBoxes: [],
attachedDocuments: [],
wellId: currentWellId
};

// Извлекаем керновые ящики и документы УНИВЕРСАЛЬНЫМ МЕТОДОМ с фильтрацией по скважине
if (currentWellId && currentWellId !== 'Не указано') {
console.log(`Извлечение дополнительных данных для скважины ${currentWellId}`);

// Используем очищенный XML контент для быстрого поиска
const serializer = new XMLSerializer();
const xmlContentString = serializer.serializeToString(xmlDoc);

// Извлекаем керновые ящики УНИВЕРСАЛЬНЫМ МЕТОДОМ с фильтрацией
console.log(`Извлечение керновых ящиков для скважины ${currentWellId}`);
const coreBoxes = extractCoreBoxesFast(xmlContentString, currentWellId);
wellData.coreBoxes = coreBoxes;
console.log(`Найдено керновых ящиков: ${coreBoxes.length}`);

// Извлекаем документы УНИВЕРСАЛЬНЫМ МЕТОДОМ с фильтрацией
console.log(`Извлечение документов для скважины ${currentWellId}`);
const documents = extractAttachedDocumentsFast(xmlContentString, currentWellId);
wellData.attachedDocuments = documents;
console.log(`Найдено документов: ${documents.length}`);

// Связываем документы с керновыми ящиками
console.log(`Связывание документов с керновыми ящиками для скважины ${currentWellId}`);
wellData.coreBoxes = linkDocumentsToCoreBoxes(
coreBoxes,
documents,
xmlContentString,
currentWellId
);
} else {
console.log('ID скважины не найден, пропускаем извлечение керновых ящиков и документов');
}

return { isMulti: false, data: wellData };

} catch (error) {
console.error('Ошибка парсинга одиночного файла:', error);
throw error;
}
}
```

2. Обновленная функция loadWellData с поддержкой мульти-скважин для всех типов файлов

```javascript
// Загрузка данных о скважине из файла
async function loadWellData(file, fileNumber) {
showLoading('Открытие архива...');
const wellDataElement = document.getElementById('wellData');
wellDataElement.innerHTML = '<div class="loading"><p>Загрузка данных...</p></div>';

try {
console.log('Начало загрузки файла:', file.name);
updateLoadingText('Чтение архива...');

const fileObj = await file.handle.getFile();
console.log('Размер файла:', fileObj.size, 'байт');

if (fileObj.size > 1500 * 1024 * 1024) {
throw new Error('Файл слишком большой для обработки в браузере (максимум 1500 МБ)');
}

updateLoadingText('Распаковка архива...');
const arrayBuffer = await fileObj.arrayBuffer();
console.log('ArrayBuffer прочитан, размер:', arrayBuffer.byteLength);

updateLoadingText('Обработка данных...');
const zip = await JSZip.loadAsync(arrayBuffer);
console.log('ZIP распакован, файлов:', Object.keys(zip.files).length);

// Сохраняем текущий ZIP архив для скачивания файлов
currentZip = zip;
console.log('ZIP архив сохранен для скачивания файлов');

const multiFiles = [];
let otherTargetFile = null;
let fileType = '';

updateProgress(10, 'Поиск файлов с данными...');

// Ищем файлы в архиве
for (const filename in zip.files) {
const zipEntry = zip.files[filename];
if (zipEntry.dir || filename.endsWith('/')) continue;

if (filename.startsWith('904~')) {
multiFiles.push({
name: filename,
entry: zipEntry,
size: zipEntry._data.uncompressedSize
});
} else if (filename.startsWith('906~') && !otherTargetFile) {
otherTargetFile = zipEntry;
fileType = 'all';
} else if (filename.startsWith('911~') && !otherTargetFile) {
otherTargetFile = zipEntry;
fileType = 'final';
} else if (filename.startsWith('909~') && !otherTargetFile) {
otherTargetFile = zipEntry;
fileType = 'primary';
} else if (!otherTargetFile && (filename.endsWith('.inu') || filename.includes('~'))) {
// Любой другой INU файл
otherTargetFile = zipEntry;
fileType = 'unknown';
}
}

// ОБРАБОТКА МУЛЬТИ-ФАЙЛОВ (904~)
if (multiFiles.length > 0) {
console.log(`Найдено мульти-файлов 904~: ${multiFiles.length}`);

const allWells = [];
let processedFiles = 0;

// ОПТИМИЗАЦИЯ: Ограничиваем количество одновременно обрабатываемых файлов
const MAX_CONCURRENT_FILES = 2;

const processFileBatch = async (startIndex) => {
const endIndex = Math.min(startIndex + MAX_CONCURRENT_FILES, multiFiles.length);
const batch = multiFiles.slice(startIndex, endIndex);

const batchPromises = batch.map(async (multiFile, index) => {
try {
const fileIndex = startIndex + index;
updateProgress(10 + (fileIndex / multiFiles.length) * 70, `Чтение файла ${fileIndex + 1} из ${multiFiles.length}`);

// ОПТИМИЗАЦИЯ: Используем быстрый метод чтения
console.time(`Read file ${multiFile.name}`);
const xmlContent = await readZipFileFast(multiFile.entry);
console.timeEnd(`Read file ${multiFile.name}`);

// ОБНОВЛЯЕМ ПРОГРЕСС
updateProgress(10 + (fileIndex / multiFiles.length) * 70 + 10, `Обработка файла ${fileIndex + 1}`);

// Сохраняем в кеш
fileCache.set(multiFile.name, xmlContent);

// Извлекаем метаданные скважин
console.time(`Extract wells from ${multiFile.name}`);
const wellsFromFile = extractWellsMetadataFromContent(xmlContent);
console.timeEnd(`Extract wells from ${multiFile.name}`);

console.log(`Из файла ${multiFile.name} извлечено скважин:`, wellsFromFile.length);

// ОБНОВЛЯЕМ ПРОГРЕСС ПОСЛЕ ОБРАБОТКИ
updateProgress(10 + (fileIndex / multiFiles.length) * 70 + 20, `Завершено ${fileIndex + 1}/${multiFiles.length}`);

return wellsFromFile.map(well => ({
...well,
sourceFile: multiFile.name
}));

} catch (error) {
console.error(`Ошибка обработки файла ${multiFile.name}:`, error);
return [];
}
});

const batchResults = await Promise.all(batchPromises);

// Объединяем результаты
for (const wells of batchResults) {
for (const well of wells) {
if (!allWells.some(w => w.wellId === well.wellId)) {
allWells.push(well);
}
}
}

processedFiles += batch.length;

// Рекурсивно обрабатываем следующую партию
if (processedFiles < multiFiles.length) {
// Даем браузеру "подышать" между батчами
await new Promise(resolve => setTimeout(resolve, 50));
return processFileBatch(processedFiles);
}
};

// Запускаем обработку батчами
await processFileBatch(0);

updateProgress(85, 'Формирование списка скважин...');
console.log(`Всего уникальных скважин: ${allWells.length}`);

if (allWells.length > 0) {
currentMultiWellData = {
wells: allWells,
fileNames: multiFiles.map(f => f.name)
};

updateProgress(100, 'Готово!');
setTimeout(() => {
hideLoading();
currentMultiFileName = file.name;
showWellsList(allWells, file.name, multiFiles.length);
wellDataElement.innerHTML = '<div class="loading"><p>Выберите скважину из списка</p></div>';
}, 100);

} else {
hideLoading();
throw new Error('Не удалось извлечь данные скважин из файлов 904~');
}

}
// ОБРАБОТКА ОДИНОЧНЫХ ФАЙЛОВ (906~, 911~, 909~ и других) С ПОДДЕРЖКОЙ МУЛЬТИ-СКВАЖИН
else if (otherTargetFile) {
// Обработка обычных файлов (906~, 911~, 909~ и других)
updateProgress(50, 'Обработка файла данных...');
console.log('Найден INU файл:', otherTargetFile.name, 'Размер:', otherTargetFile._data.uncompressedSize);

// Увеличиваем лимит размера INU файла до 2000MB
if (otherTargetFile._data.uncompressedSize > 2000 * 1024 * 1024) {
throw new Error('INU файл слишком большой для обработки в браузере (максимум 2000 МБ)');
}

// Читаем XML данные
updateLoadingText('Чтение XML данных...');
console.log('Чтение XML...');
const xmlContent = await otherTargetFile.async('text');
console.log('XML прочитан, длина:', xmlContent.length);

// Парсинг XML данных с обработкой ошибок
updateProgress(80, 'Парсинг XML...');
console.log('Парсинг XML...');

const parser = new DOMParser();
const xmlDoc = parser.parseFromString(cleanXMLContent(xmlContent), 'text/xml');

const parseError = xmlDoc.getElementsByTagName('parsererror')[0];
if (parseError) {
throw new Error('Ошибка парсинга XML: файл поврежден или имеет неверный формат');
}

// Используем обновленную функцию парсинга с поддержкой мульти-скважин
const parseResult = await parseSingleWellData(xmlDoc, fileType, file.name);

if (parseResult.isMulti) {
// Если файл содержит несколько скважин, показываем список
console.log(`Файл содержит ${parseResult.wellCount} скважин, показываем список`);

currentMultiWellData = {
wells: parseResult.wells,
xmlContent: xmlContent,
fileNames: [file.name],
wellCount: parseResult.wellCount
};

updateProgress(100, 'Готово!');
setTimeout(() => {
hideLoading();
currentMultiFileName = file.name;
showWellsList(parseResult.wells, file.name, 1);
wellDataElement.innerHTML = '<div class="loading"><p>Выберите скважину из списка</p></div>';
}, 100);
} else {
// Одиночная скважина - отображаем сразу
xmlData = parseResult.data;

updateProgress(100, 'Готово!');
setTimeout(() => {
hideLoading();
displayWellData(fileType, fileNumber, file.name);
console.log('Данные отображены');
}, 500);
}

} else {
hideLoading();
throw new Error('В архиве не найден файл с данными (начинающийся с 904~, 906~, 909~ или 911~)');
}

} catch (error) {
hideLoading();
console.error('Ошибка загрузки данных:', error);
wellDataElement.innerHTML = `
<div class="error">
<p><strong>Ошибка загрузки данных:</strong> ${error.message}</p>
<p><strong>Поддерживаемые размеры:</strong></p>
<ul style="text-align: left; margin: 10px 0;">
<li>ZIP архивы: до 1500 МБ</li>
<li>INU файлы: до 2000 МБ</li>
</ul>
<p>Попробуйте выбрать другой файл или обратитесь к администратору</p>
</div>
`;
}
}
```

3. Обновленная функция loadWellDataFromFile для ручного выбора файлов

```javascript
// Загрузка данных о скважине из файла (ручной выбор)
async function loadWellDataFromFile(file, fileNumber) {
// Очищаем список при загрузке нового файла
clearFileList();
showLoading('Открытие файла...');
const wellDataElement = document.getElementById('wellData');
wellDataElement.innerHTML = '<div class="loading"><p>Загрузка данных...</p></div>';

try {
if (file.size > 2000 * 1024 * 1024) {
throw new Error('Файл слишком большой для обработки в браузере (максимум 2000 МБ)');
}

updateProgress(30, 'Чтение файла...');
const fileContent = await file.text();

updateProgress(70, 'Обработка XML...');
const cleanedContent = cleanXMLContent(fileContent);

// Определяем тип файла по имени
let fileType = '';
if (file.name.startsWith('906~')) {
fileType = 'all';
} else if (file.name.startsWith('911~')) {
fileType = 'final';
} else if (file.name.startsWith('909~')) {
fileType = 'primary';
} else if (file.name.startsWith('904~')) {
fileType = 'multi';
} else {
fileType = 'unknown';
}

// Парсинг XML данных
updateProgress(90, 'Парсинг данных...');

const parser = new DOMParser();
const xmlDoc = parser.parseFromString(cleanedContent, 'text/xml');

const parseError = xmlDoc.getElementsByTagName('parsererror')[0];
if (parseError) {
throw new Error('Ошибка парсинга XML: файл поврежден или имеет неверный формат');
}

// Используем обновленную функцию парсинга с поддержкой мульти-скважин
const parseResult = await parseSingleWellData(xmlDoc, fileType, file.name);

updateProgress(100, 'Готово!');

setTimeout(() => {
hideLoading();

if (parseResult.isMulti) {
// Если файл содержит несколько скважин, показываем список
currentMultiWellData = {
wells: parseResult.wells,
xmlContent: cleanedContent,
fileNames: [file.name],
wellCount: parseResult.wellCount
};

currentMultiFileName = file.name;
showWellsList(parseResult.wells, file.name, 1);
wellDataElement.innerHTML = '<div class="loading"><p>Выберите скважину из списка</p></div>';
} else {
// Одиночная скважина - сразу отображаем данные
xmlData = parseResult.data;
displayWellData(fileType, fileNumber, file.name);
}
}, 500);

} catch (error) {
hideLoading();
console.error('Ошибка загрузки данных:', error);
wellDataElement.innerHTML = `
<div class="error">
<p><strong>Ошибка загрузки данных:</strong> ${error.message}</p>
<p><strong>Поддерживаемые размеры:</strong></p>
<ul style="text-align: left; margin: 10px 0;">
<li>ZIP архивы: до 1500 МБ</li>
<li>INU файлы: до 2000 МБ</li>
</ul>
<p>Попробуйте выбрать другой файл или архив</p>
</div>
`;
}
}
```

Теперь система корректно обрабатывает:

1. Файлы 904~ - как мульти-файлы (как и раньше)
2. Файлы 906~, 911~, 909~ и другие - автоматически определяет количество скважин и показывает список если скважин больше одной
3. Правильно фильтрует данные - интервалы, опробование, керновые ящики и документы привязываются только к текущей скважине
4. Сохраняет обратную совместимость - одиночные скважины отображаются сразу
Понял! Нужно доработать функцию extractCoreBoxesFast чтобы она фильтровала керновые ящики только для конкретной скважины. Вот исправленная версия:

Исправленная функция extractCoreBoxesFast

```javascript
// Функция для извлечения данных о керновых ящиках с фильтрацией по скважине
function extractCoreBoxesFast(xmlContent, wellId) {
const coreBoxes = [];

try {
console.log(`Поиск керновых ящиков для скважины ${wellId} (универсальный метод)`);

// 1. Создаем карту PO_DRIFTING_PODR (скважины) -> ID_POINT_OBSERV_PODR
const driftingToPointMap = new Map();
const driftingRegex = /<MR_DBA\.PO_DRIFTING_PODR>[\s\S]*?<\/MR_DBA\.PO_DRIFTING_PODR>/g;
const driftingMatches = xmlContent.match(driftingRegex) || [];

for (const driftingMatch of driftingMatches) {
const driftingId = extractValue(driftingMatch, 'ID_PO_DRIFTING_PODR');
const pointObservId = extractValue(driftingMatch, 'ID_POINT_OBSERV_PODR');
if (driftingId && pointObservId) {
driftingToPointMap.set(driftingId, pointObservId);
}
}

console.log(`Карта скважин -> точек наблюдения: ${driftingToPointMap.size}`);

// 2. Создаем карту PO_DRIFTING_CORE_POCR (керны) -> ID_PO_DRIFTING_POCR
const coreToDriftingMap = new Map();
const coreRegex = /<MR_DBA\.PO_DRIFTING_CORE_POCR>[\s\S]*?<\/MR_DBA\.PO_DRIFTING_CORE_POCR>/g;
const coreMatches = xmlContent.match(coreRegex) || [];

for (const coreMatch of coreMatches) {
const coreId = extractValue(coreMatch, 'ID_PO_DRIFTING_CORE_POCR');
const driftingId = extractValue(coreMatch, 'ID_PO_DRIFTING_POCR');
if (coreId && driftingId) {
coreToDriftingMap.set(coreId, driftingId);
}
}

console.log(`Карта кернов -> скважин: ${coreToDriftingMap.size}`);

// 3. Ищем керновые ящики и связываем со скважиной
const boxRegex = /<MR_DBA\.PO_DRIFTING_BOX_PODB>[\s\S]*?<\/MR_DBA\.PO_DRIFTING_BOX_PODB>/g;
const boxMatches = xmlContent.match(boxRegex) || [];

console.log(`Найдено керновых ящиков всего: ${boxMatches.length}`);

for (const boxMatch of boxMatches) {
const boxId = extractValue(boxMatch, 'ID_PO_DRIFTING_BOX_PODB');
const coreId = extractValue(boxMatch, 'ID_PO_DRIFTING_CORE_PODB');
const boxNumber = extractValue(boxMatch, 'NUMBER_PP_BOX_PODB') || extractValue(boxMatch, 'BOX_NUMBER_PODB');
const intervalFrom = parseFloat(extractValue(boxMatch, 'PO_DRIFTING_FROM_PODB') || extractValue(boxMatch, 'INTERV_FROM_PODB') || '0');
const intervalTo = parseFloat(extractValue(boxMatch, 'PO_DRIFTING_TO_PODB') || extractValue(boxMatch, 'INTERV_TO_PODB') || '0');
const thickness = parseFloat(extractValue(boxMatch, 'THICKNESS_PODB') || '0');
const basalHorizon = extractValue(boxMatch, 'BASAL_HORIZ_PODB');
const description = extractValue(boxMatch, 'COMMENT_PODB') || extractValue(boxMatch, 'DESCRIPTION_PODB');
const inputDate = formatDate(extractValue(boxMatch, 'DATE_INPUT_PODB'));

console.log(`Ящик ${boxNumber}: coreId=${coreId}, from=${intervalFrom}, to=${intervalTo}`);

// Проверяем связь со скважиной через цепочку: Ящик -> Керн -> Скважина -> Точка наблюдения
let isRelated = false;

if (coreId && coreToDriftingMap.has(coreId)) {
const driftingId = coreToDriftingMap.get(coreId);
if (driftingId && driftingToPointMap.has(driftingId)) {
const pointObservId = driftingToPointMap.get(driftingId);
// Если точка наблюдения совпадает с ID скважины
if (pointObservId === wellId) {
isRelated = true;
console.log(`✓ Ящик ${boxNumber} принадлежит скважине ${wellId} через цепочку`);
} else {
console.log(`✗ Ящик ${boxNumber} принадлежит другой скважине ${pointObservId}`);
}
}
}

// Если не нашли связь через цепочку, проверяем прямую связь
if (!isRelated) {
// Проверяем прямую связь ящика со скважиной
const directWellId = extractValue(boxMatch, 'ID_POINT_OBSERV_PODB');
if (directWellId && directWellId === wellId) {
isRelated = true;
console.log(`✓ Ящик ${boxNumber} принадлежит скважине ${wellId} через прямую связь`);
} else if (directWellId) {
console.log(`✗ Ящик ${boxNumber} принадлежит другой скважине ${directWellId} (прямая связь)`);
}
}

// ДОПОЛНИТЕЛЬНАЯ ПРОВЕРКА: Проверяем связь через керн и проходку
if (!isRelated && coreId) {
// Ищем информацию о керне
const coreRegex = new RegExp(`<MR_DBA\\.PO_DRIFTING_CORE_POCR>(?:[^<]|<(?!\\/MR_DBA\\.PO_DRIFTING_CORE_POCR>))*<ID_PO_DRIFTING_CORE_POCR>${coreId}<\\/ID_PO_DRIFTING_CORE_POCR>(?:[^<]|<(?!\\/MR_DBA\\.PO_DRIFTING_CORE_POCR>))*<\\/MR_DBA\\.PO_DRIFTING_CORE_POCR>`, 's');
const coreMatch = xmlContent.match(coreRegex);

if (coreMatch) {
const coreDriftingId = extractValue(coreMatch[0], 'ID_PO_DRIFTING_POCR');
if (coreDriftingId && driftingToPointMap.has(coreDriftingId)) {
const coreWellId = driftingToPointMap.get(coreDriftingId);
if (coreWellId === wellId) {
isRelated = true;
console.log(`✓ Ящик ${boxNumber} принадлежит скважине ${wellId} через керн`);
}
}
}
}

// ДЛЯ ОДИНОЧНЫХ ФАЙЛОВ (906, 911, 909): если не нашли связь, но файл один - НЕ считаем связанным
// УБИРАЕМ автоматическое связывание для одиночных файлов
if (!isRelated) {
console.log(`✗ Ящик ${boxNumber} не принадлежит скважине ${wellId}, пропускаем`);
continue; // Пропускаем ящики, не принадлежащие текущей скважине
}

if (isRelated) {
const basalText = basalHorizon === 'Д' ? 'Базальный горизонт' :
basalHorizon === 'Н' ? 'Не базальный' :
basalHorizon || 'Не указано';

coreBoxes.push({
id: boxId,
boxNumber: boxNumber || 'Не указан',
coreId: coreId,
intervalFrom: intervalFrom,
intervalTo: intervalTo,
thickness: thickness,
interval: `${formatNumber(intervalFrom)} - ${formatNumber(intervalTo)} м`,
basalHorizon: basalText,
description: description || '',
inputDate: inputDate,
relatedDocuments: []
});

console.log(`✓ Найден керновый ящик ${boxNumber} для скважины ${wellId}`);
}
}

// Сортируем ящики по интервалу
coreBoxes.sort((a, b) => a.intervalFrom - b.intervalFrom);

console.log(`Найдено керновых ящиков для скважины ${wellId}: ${coreBoxes.length}`);

} catch (error) {
console.error('Ошибка извлечения керновых ящиков:', error);
}

return coreBoxes;
}
```

Дополнительно нужно обновить функцию linkDocumentsToCoreBoxes для правильной фильтрации:

```javascript
// Функция для связывания документов с керновыми ящиками через прямые связи
function linkDocumentsToCoreBoxes(coreBoxes, documents, xmlContent, wellId) {
try {
console.log(`=== СВЯЗЫВАНИЕ ДОКУМЕНТОВ С ЯЩИКАМИ ДЛЯ СКВАЖИНЫ ${wellId} ===`);

// 1. Фильтруем документы - берем только те, что принадлежат этой скважине
const wellDocuments = documents.filter(doc => {
// Документ принадлежит скважине если:
// - relationType === 'direct' (прямая связь)
// - или wellInfo соответствует текущей скважине
const currentWellName = xmlData ? xmlData.wellName : '';
const currentLineNumber = xmlData ? xmlData.lineNumber : '';

let currentWellDisplay = '';
if (currentLineNumber && currentLineNumber !== 'Не указано' && currentWellName) {
currentWellDisplay = `л${currentLineNumber} - ${currentWellName}`;
} else if (currentWellName) {
currentWellDisplay = currentWellName;
}

if (doc.relationType === 'direct') {
return true;
}

if (doc.wellInfo && currentWellDisplay && doc.wellInfo === currentWellDisplay) {
return true;
}

return false;
});

console.log(`Документов, принадлежащих скважине ${wellId}: ${wellDocuments.length}`);

// 2. Создаем карту документов по targetObjectId
const documentsByTargetObject = new Map();
wellDocuments.forEach(doc => {
if (doc.targetObjectId) {
if (!documentsByTargetObject.has(doc.targetObjectId)) {
documentsByTargetObject.set(doc.targetObjectId, []);
}
documentsByTargetObject.get(doc.targetObjectId).push(doc);
}
});

console.log(`Документы сгруппированы по targetObjectId:`, Array.from(documentsByTargetObject.keys()));

// 3. Связываем документы с ящиками через прямые связи targetObjectId
coreBoxes.forEach(box => {
const relatedDocs = [];
const boxId = box.id;

console.log(`Обработка ящика ${box.boxNumber} (ID: ${boxId})`);

// Ищем документы, которые напрямую привязаны к этому ящику
if (documentsByTargetObject.has(boxId)) {
const boxDocuments = documentsByTargetObject.get(boxId);
boxDocuments.forEach(doc => {
relatedDocs.push({
id: doc.id,
docDcaaId: doc.docDcaaId,
fileName: doc.fileName,
fileType: doc.fileType,
downloadFileName: doc.downloadFileName,
blobPath: doc.blobPath,
description: doc.description,
relationType: 'direct_box_link',
relationNote: `Прямая привязка к ящику ${box.boxNumber}`
});
console.log(`✓ Документ "${doc.fileName}" напрямую привязан к ящику ${box.boxNumber}`);
});
} else {
console.log(`✗ Для ящика ${box.boxNumber} не найдено напрямую привязанных документов`);
}

box.relatedDocuments = relatedDocs;
});

// 4. Сводная статистика
const boxesWithDocs = coreBoxes.filter(box => box.relatedDocuments.length > 0).length;
const totalLinkedDocuments = coreBoxes.reduce((count, box) => count + box.relatedDocuments.length, 0);

console.log(`=== СВОДКА СВЯЗЕЙ ДЛЯ СКВАЖИНЫ ${wellId} ===`);
console.log(`Всего ящиков: ${coreBoxes.length}`);
console.log(`Ящиков с документами: ${boxesWithDocs}`);
console.log(`Всего связей документов с ящиками: ${totalLinkedDocuments}`);

} catch (error) {
console.error('Ошибка связывания документов с керновыми ящиками:', error);
}

return coreBoxes;
}
```

Также обновим функцию extractAttachedDocumentsFast для лучшей фильтрации:

```javascript
// Функция для извлечения вложенных документов для всех типов файлов с фильтрацией по скважине
function extractAttachedDocumentsFast(xmlContent, wellId) {
const documents = [];

try {
console.log(`=== ПОИСК ДОКУМЕНТОВ ДЛЯ СКВАЖИНЫ ${wellId} ===`);

// 1. Находим информацию о текущей скважине
const wellRegex = new RegExp(`<MR_DBA\\.POINT_OBSERV_POAA>(?:[^<]|<(?!\\/MR_DBA\\.POINT_OBSERV_POAA>))*<ID_POINT_OBSERV_POAA>${wellId}<\\/ID_POINT_OBSERV_POAA>(?:[^<]|<(?!\\/MR_DBA\\.POINT_OBSERV_POAA>))*<\\/MR_DBA\\.POINT_OBSERV_POAA>`, 's');
const wellMatch = xmlContent.match(wellRegex);

let currentWellName = '';
let currentLineNumber = '';

if (wellMatch) {
currentWellName = extractValue(wellMatch[0], 'NAME_FULL_POAA') || 'Без названия';
const lineId = extractValue(wellMatch[0], 'ID_LINE_COMMON_POAA');

if (lineId) {
const lineRegex = new RegExp(`<MR_DBA\\.LINE_COMMON_LNCM>(?:[^<]|<(?!\\/MR_DBA\\.LINE_COMMON_LNCM>))*<ID_LINE_COMMON_LNCM>${lineId}<\\/ID_LINE_COMMON_LNCM>(?:[^<]|<(?!\\/MR_DBA\\.LINE_COMMON_LNCM>))*<\\/MR_DBA\\.LINE_COMMON_LNCM>`, 's');
const lineMatch = xmlContent.match(lineRegex);
if (lineMatch) {
currentLineNumber = extractValue(lineMatch[0], 'NAME_FULL_LNCM') || '';
}
}
}

// Форматируем отображаемое название текущей скважины
let currentWellDisplay = '';
if (currentLineNumber && currentLineNumber !== 'Не указано' && currentWellName) {
currentWellDisplay = `л${currentLineNumber} - ${currentWellName}`;
} else if (currentWellName) {
currentWellDisplay = currentWellName;
}

console.log(`Текущая скважина: ${currentWellDisplay}`);

// ... остальной код функции extractAttachedDocumentsFast ...

// В конце, после извлечения всех документов, добавляем проверку принадлежности
documents.forEach(doc => {
// Определяем принадлежность документа к текущей скважине
if (doc.wellInfo && currentWellDisplay && doc.wellInfo === currentWellDisplay) {
doc.relationType = 'direct';
doc.relationNote = `Привязан к текущей скважине ${currentWellDisplay}`;
} else if (doc.wellInfo) {
doc.relationType = 'unrelated';
doc.relationNote = `Принадлежит другой скважине: ${doc.wellInfo}`;
} else if (doc.targetObjectId) {
// Проверяем связь через targetObjectId
const wellInfo = getWellInfoForDocument(doc.targetObjectId, xmlContent);
if (wellInfo && wellInfo.displayName === currentWellDisplay) {
doc.relationType = 'direct';
doc.relationNote = `Привязан к текущей скважине через объект ${doc.targetObjectId}`;
doc.wellInfo = currentWellDisplay;
} else if (wellInfo) {
doc.relationType = 'unrelated';
doc.relationNote = `Принадлежит другой скважине: ${wellInfo.displayName}`;
doc.wellInfo = wellInfo.displayName;
}
}
});

} catch (error) {
console.error('Ошибка извлечения документов:', error);
}

return documents;
}
```

Теперь система будет:

1. Фильтровать керновые ящики только для текущей скважины
2. Не показывать ящики других скважин из того же файла
3. Корректно связывать документы только с ящиками текущей скважины
4. Логировать процесс для отладки связей между ящиками и скважинами

Теперь в таблице с керновыми ящиками будут отображаться только ящики, принадлежащие выбранной скважине.
223
Прикрепления:
Отлично, я изучил схему данных и вижу проблему! Проблема в том, что текущий код неправильно определяет связи между документами и скважинами в мульти-файлах.

## Основные проблемы в текущем коде:

### 1. **Неправильная логика связывания документов со скважинами**
В функции `extractAttachedDocumentsFast` код пытается найти связи через `wellDataObjects`, но не учитывает полную цепочку через `DOCUM_OBJ_SYS_DCOS`.

### 2. **Отсутствие анализа связей DOCUM_OBJ_SYS_DCOS**
Код не использует таблицу `DOCUM_OBJ_SYS_DCOS` для определения, к каким конкретно объектам скважин привязаны документы.

### 3. **Неправильное определение принадлежности документов**
В мульти-файлах документы могут принадлежать разным скважинам, но текущий код этого не учитывает.

## Предлагаю исправить следующим образом:

### 1. Добавить функцию анализа связей DOCUM_OBJ_SYS_DCOS:

```javascript
function analyzeDocumentLinks(xmlContent, wellId) {
const documentLinks = new Map();

try {
// Ищем все связи документов с объектами
const dcosRegex = /<MR_DBA\.DOCUM_OBJ_SYS_DCOS>[\s\S]*?<\/MR_DBA\.DOCUM_OBJ_SYS_DCOS>/g;
const dcosMatches = xmlContent.match(dcosRegex) || [];

for (const dcosMatch of dcosMatches) {
const documentId = extractValue(dcosMatch, 'ID_DOCUM_DCOS');
const objectId = extractValue(dcosMatch, 'ID_OBJECT_DCOS');

if (documentId && objectId) {
// Находим, к какой скважине принадлежит этот объект
const targetWellId = findWellIdByObjectId(objectId, xmlContent, wellId);
if (targetWellId) {
documentLinks.set(documentId, {
objectId: objectId,
wellId: targetWellId
});
}
}
}
} catch (error) {
console.error('Ошибка анализа связей документов:', error);
}

return documentLinks;
}
```

### 2. Функция поиска ID скважины по ID объекта:

```javascript
function findWellIdByObjectId(objectId, xmlContent, currentWellId) {
try {
// Пробуем найти через PO_DOCUM_PODC (документирование)
const podcRegex = /<MR_DBA\.PO_DOCUM_PODC>[\s\S]*?<\/MR_DBA\.PO_DOCUM_PODC>/g;
const podcMatches = xmlContent.match(podcRegex) || [];

for (const podcMatch of podcMatches) {
const podcId = extractValue(podcMatch, 'ID_PO_DOCUM_PODC');
if (podcId === objectId) {
return extractValue(podcMatch, 'ID_POINT_OBSERV_PODC');
}
}

// Пробуем найти через PO_DRIFTING_BOX_PODB (ящики)
const podbRegex = /<MR_DBA\.PO_DRIFTING_BOX_PODB>[\s\S]*?<\/MR_DBA\.PO_DRIFTING_BOX_PODB>/g;
const podbMatches = xmlContent.match(podbRegex) || [];

for (const podbMatch of podbMatches) {
const podbId = extractValue(podbMatch, 'ID_PO_DRIFTING_BOX_PODB');
if (podbId === objectId) {
// Для ящиков связь через керн и проходку
const coreId = extractValue(podbMatch, 'ID_PO_DRIFTING_CORE_PODB');
if (coreId) {
return findWellIdFromCore(coreId, xmlContent);
}
}
}

// Пробуем найти через PO_DOCUM_KRF_PODK (интервалы с фото)
const podkRegex = /<MR_DBA\.PO_DOCUM_KRF_PODK>[\s\S]*?<\/MR_DBA\.PO_DOCUM_KRF_PODK>/g;
const podkMatches = xmlContent.match(podkRegex) || [];

for (const podkMatch of podkMatches) {
const podkId = extractValue(podkMatch, 'ID_PO_DOCUM_KRF_PODK');
if (podkId === objectId) {
const docId = extractValue(podbMatch, 'ID_PO_DOCUM_PODK');
if (docId) {
// Ищем документирование для этого интервала
for (const podcMatch of podcMatches) {
const podcId = extractValue(podcMatch, 'ID_PO_DOCUM_PODC');
if (podcId === docId) {
return extractValue(podcMatch, 'ID_POINT_OBSERV_PODC');
}
}
}
}
}

} catch (error) {
console.error('Ошибка поиска скважины по объекту:', error);
}

return null;
}
```

### 3. Обновить функцию извлечения документов:

```javascript
function extractAttachedDocumentsFast(xmlContent, wellId) {
const documents = [];

try {
console.log(`=== ПОИСК ДОКУМЕНТОВ ДЛЯ СКВАЖИНЫ ${wellId} ===`);

// 1. Анализируем связи документов с объектами
const documentLinks = analyzeDocumentLinks(xmlContent, wellId);
console.log(`Найдено связей документов: ${documentLinks.size}`);

// 2. Создаем карту DOCUM_DCAA
const documentInfoMap = new Map();
const documentRegex = /<MR_DBA\.DOCUM_DCAA>[\s\S]*?<\/MR_DBA\.DOCUM_DCAA>/g;
const documentMatches = xmlContent.match(documentRegex) || [];

for (const documentMatch of documentMatches) {
const documentId = extractValue(documentMatch, 'ID_DOCUM_DCAA');
const comment = extractValue(documentMatch, 'COMMENT_DCAA');
const name = extractValue(documentMatch, 'NAME_DOC_DCAA');
if (documentId) {
documentInfoMap.set(documentId, {
comment: comment || 'Нет описания',
name: name || `Документ ${documentId}`
});
}
}

// 3. Создаем карту CONTENT_LOBA
const contentBlobMap = new Map();
const contentRegex = /<MR_DBA\.CONTENT_LOBA>[\s\S]*?<\/MR_DBA\.CONTENT_LOBA>/g;
const contentMatches = xmlContent.match(contentRegex) || [];

for (const contentMatch of contentMatches) {
const objectId = extractValue(contentMatch, 'ID_OBJECT_LOBA');
const blobPath = extractValue(contentMatch, 'BLOB_VALUE_LOBA');
if (objectId && blobPath) {
contentBlobMap.set(objectId, blobPath);
}
}

// 4. Ищем все файлы в DD_OBJECT_DDOB
const objectRegex = /<MR_DBA\.DD_OBJECT_DDOB>[\s\S]*?<\/MR_DBA\.DD_OBJECT_DDOB>/g;
const objectMatches = xmlContent.match(objectRegex) || [];

console.log(`Найдено записей DD_OBJECT_DDOB: ${objectMatches.length}`);

for (const objectMatch of objectMatches) {
const ddoBbjectId = extractValue(objectMatch, 'ID_DD_OBJECT_DDOB');
const idObjectDDOB = extractValue(objectMatch, 'ID_OBJECT_DDOB'); // Это ID_DOCUM_DCAA!
const fileName = extractValue(objectMatch, 'COMMENT_DDOB');
const fileType = extractValue(objectMatch, 'TYPE_DATA_DDOB');
const fileExtension = extractValue(objectMatch, 'EXPAND_DDOB');
const inputDate = formatDate(extractValue(objectMatch, 'DATE_INPUT_DDOB'));
const author = extractValue(objectMatch, 'VVOD_ID_CONTRACTOR_DDOB');

// Получаем данные из DOCUM_DCAA
let description = 'Нет описания';
let displayName = fileName || `Документ ${ddoBbjectId}`;

if (idObjectDDOB && documentInfoMap.has(idObjectDDOB)) {
const docInfo = documentInfoMap.get(idObjectDDOB);
description = docInfo.comment;
displayName = docInfo.name;
}

// Получаем путь к файлу
const blobPath = contentBlobMap.get(ddoBbjectId);

// Формируем имя файла для скачивания
let downloadFileName = fileName;
if (fileName && fileExtension) {
const baseName = fileName.replace(/\.[^/.]+$/, "");
downloadFileName = baseName + fileExtension;
} else if (fileName) {
downloadFileName = fileName;
} else {
downloadFileName = `document_${ddoBbjectId}`;
}

// ОПРЕДЕЛЯЕМ ПРИНАДЛЕЖНОСТЬ К СКВАЖИНЕ через DOCUM_OBJ_SYS_DCOS
let relationType = 'unknown';
let targetWellId = null;

if (idObjectDDOB && documentLinks.has(idObjectDDOB)) {
const link = documentLinks.get(idObjectDDOB);
targetWellId = link.wellId;

if (targetWellId === wellId) {
relationType = 'direct';
console.log(`✓ Документ "${displayName}" принадлежит скважине ${wellId}`);
} else {
relationType = 'unrelated';
console.log(`✗ Документ "${displayName}" принадлежит другой скважине ${targetWellId}`);
}
} else {
relationType = 'unknown';
console.log(`? Документ "${displayName}" не имеет связи с объектом`);
}

const documentData = {
id: ddoBbjectId,
docDcaaId: idObjectDDOB,
fileName: displayName,
fileType: fileType || 'Неизвестный тип',
fileExtension: fileExtension || '',
description: description,
blobPath: blobPath,
author: author || 'Не указан',
uploadDate: inputDate,
downloadFileName: downloadFileName,
originalFileName: fileName,
relationType: relationType,
targetWellId: targetWellId,
// Сохраняем информацию о скважине для отображения
wellInfo: targetWellId ? findWellInfoById(targetWellId, xmlContent)?.displayName : null
};

documents.push(documentData);
}

// Статистика
const relatedCount = documents.filter(d => d.relationType === 'direct').length;
const unrelatedCount = documents.filter(d => d.relationType === 'unrelated').length;
const unknownCount = documents.filter(d => d.relationType === 'unknown').length;

console.log(`=== РЕЗУЛЬТАТЫ ДЛЯ СКВАЖИНЫ ${wellId} ===`);
console.log(`Всего документов: ${documents.length}`);
console.log(`Принадлежащих скважине: ${relatedCount}`);
console.log(`Принадлежащих другим скважинам: ${unrelatedCount}`);
console.log(`Непривязанных к объектам: ${unknownCount}`);

} catch (error) {
console.error('Ошибка извлечения документов:', error);
}

return documents;
}
```

Хотите, чтобы я внедрил эти исправления в код? Это решит проблему с неправильным определением принадлежности документов в мульти-файлах.

Добавлено (2025-10-07, 13:24)
---------------------------------------------
Отлично! Проблема в том, что функция `loadWellData` пытается обработать отдельный INU файл как ZIP архив. Нужно добавить проверку типа файла и разную логику обработки для ZIP и INU файлов.

Вот исправления:

## 1. Обновить функцию `loadWellData`:

```javascript
async function loadWellData(file, fileNumber) {
// Запускаем keep-alive при начале загрузки
startKeepAlive();

showLoading('Анализ файла...');
const wellDataElement = document.getElementById('wellData');
wellDataElement.innerHTML = '<div class="loading"><p>Загрузка данных...</p></div>';

try {
console.log('Начало загрузки файла:', file.name);
updateLoadingText('Чтение файла...');
updateProgress(5, 'Подготовка...');

const fileObj = await file.handle.getFile();
console.log('Размер файла:', fileObj.size, 'байт');
console.log('Тип файла:', fileObj.type);
console.log('Имя файла:', fileObj.name);

// ОПРЕДЕЛЯЕМ ТИП ФАЙЛА
const isZipFile = fileObj.name.toLowerCase().endsWith('.zip');
const isInuFile = fileObj.name.toLowerCase().endsWith('.inu');

console.log('isZipFile:', isZipFile, 'isInuFile:', isInuFile);

if (isZipFile) {
// СУЩЕСТВУЮЩАЯ ЛОГИКА ДЛЯ ZIP АРХИВОВ
if (fileObj.size > 1500 * 1024 * 1024) {
throw new Error('Файл слишком большой для обработки в браузере (максимум 1500 МБ)');
}

updateLoadingText('Распаковка архива...');
updateProgress(15, 'Распаковка архива...');

const arrayBuffer = await Promise.race([
fileObj.arrayBuffer(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Таймаут чтения файла')), 30000)
)
]);

console.log('ArrayBuffer прочитан, размер:', arrayBuffer.byteLength);

updateLoadingText('Обработка данных...');
updateProgress(25, 'Обработка ZIP...');

const zip = await Promise.race([
JSZip.loadAsync(arrayBuffer),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Таймаут распаковки архива')), 45000)
)
]);

console.log('ZIP распакован, файлов:', Object.keys(zip.files).length);

currentZip = zip;

// ДАЛЕЕ СУЩЕСТВУЮЩАЯ ЛОГИКА ДЛЯ ZIP...
// [остальной код обработки ZIP архива остается без изменений]

} else if (isInuFile) {
// НОВАЯ ЛОГИКА ДЛЯ ОТДЕЛЬНЫХ INU ФАЙЛОВ
console.log('Обработка отдельного INU файла');
updateLoadingText('Чтение INU файла...');
updateProgress(30, 'Чтение XML данных...');

// Читаем содержимое INU файла как текст
const xmlContent = await Promise.race([
fileObj.text(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Таймаут чтения INU файла')), 30000)
)
]);

console.log('INU файл прочитан, размер:', xmlContent.length, 'символов');

// Для INU файлов создаем виртуальный "архив" с одним файлом
currentZip = null; // Сбрасываем текущий ZIP

// Определяем тип файла по имени
const fileType = getFileType(fileObj.name);
console.log('Тип INU файла:', fileType);

updateProgress(50, 'Анализ данных скважины...');

// Ищем скважины в INU файле
const wells = detectWellsInContent(xmlContent, fileObj.name);
console.log('Найдено скважин в INU файле:', wells.length);

if (wells.length === 0) {
throw new Error('В INU файле не найдено данных скважин');
}

// Сохраняем XML контент для быстрого доступа
fileCache.set(fileObj.name, xmlContent);

// ЕДИНАЯ ЛОГИКА ДЛЯ ВСЕХ СЛУЧАЕВ (как в ZIP обработке)
if (wells.length === 1) {
// Одна скважина - сразу загружаем и отображаем
const well = wells[0];
console.log(`Одна скважина найдена: ${well.wellId}, загружаем напрямую`);

updateProgress(70, 'Загрузка данных скважины...');
await parseWellDataUniversal(xmlContent, well.wellId, fileType);

updateProgress(100, 'Готово!');
setTimeout(() => {
hideLoading();
stopKeepAlive();
displayWellData(fileType, fileNumber, file.name, well.wellId);
}, 100);

} else {
// Несколько скважин - показываем список
console.log(`Найдено ${wells.length} скважин, показываем список`);

currentMultiWellData = {
wells: wells,
fileNames: [fileObj.name],
fileType: fileType,
xmlContent: xmlContent // Сохраняем XML контент для мульти-файла
};
currentMultiFileName = file.name;

updateProgress(100, 'Готово!');
setTimeout(() => {
hideLoading();
stopKeepAlive();
showWellsList(wells, file.name, 1);
updateWellHeaderForMultiFile(fileType, file.name);
wellDataElement.innerHTML = '<div class="loading"><p>Выберите скважину из списка</p></div>';
}, 100);
}

} else {
throw new Error('Неподдерживаемый формат файла. Поддерживаются только .zip и .inu файлы');
}

} catch (error) {
// Гарантированно останавливаем keep-alive при ошибке
stopKeepAlive();
hideLoading();

console.error('Ошибка загрузки данных:', error);
wellDataElement.innerHTML = `
<div class="error">
<p><strong>Ошибка загрузки данных:</strong> ${error.message}</p>
<p>Попробуйте выбрать другой файл</p>
${error.message.includes('Таймаут') ? `
<div style="margin-top: 15px; padding: 10px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px;">
<strong>Совет:</strong> Попробуйте выбрать меньший файл или проверьте стабильность интернет-соединения
</div>
` : ''}
</div>
`;
}
}
```

## 2. Обновить функцию `selectFileManually` для поддержки INU:

```javascript
async function selectFileManually() {
try {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.inu,.zip'; // Разрешаем оба формата
input.onchange = async (e) => {
const file = e.target.files[0];
if (file) {
// ОЧИСТКА СПИСКА ПРИ ВЫБОРЕ НОВОГО ФАЙЛА
clearFileList();
clearCaches();

// Создаем временный file handle-like объект
const tempFile = {
name: file.name,
handle: { getFile: () => Promise.resolve(file) }
};

await loadWellData(tempFile, 0);
}
};
input.click();
} catch (error) {
console.error('Ошибка выбора файла:', error);
alert('Ошибка при выборе файла: ' + error.message);
}
}
```

## 3. Обновить функцию `parseWellDataFromMulti` для работы с INU:

```javascript
async function parseWellDataFromMulti(wellId, signal = null) {
// [существующий код проверок...]

try {
updateProgress(10, 'Поиск информации о скважине...');

// ПРОВЕРЯЕМ отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

const targetWellMeta = currentMultiWellData.wells.find(w => w.wellId === wellId);
if (!targetWellMeta) {
throw new Error(`Скважина с ID ${wellId} не найдена в списке`);
}

let xmlContent = '';
let sourceFile = '';

// Случай 1: ZIP архив (данные в fileCache)
if (targetWellMeta.sourceFile && fileCache.has(targetWellMeta.sourceFile)) {
sourceFile = targetWellMeta.sourceFile;
updateProgress(30, 'Загрузка данных из кеша...');
xmlContent = fileCache.get(sourceFile);
}
// Случай 2: INU файл (данные в currentMultiWellData.xmlContent)
else if (currentMultiWellData.xmlContent) {
updateProgress(30, 'Загрузка данных из INU файла...');
xmlContent = currentMultiWellData.xmlContent;
sourceFile = 'current_inu_file';
}
// Случай 3: Не удалось определить источник
else {
throw new Error('Не удалось определить источник данных для скважины');
}

// [дальнейшая обработка без изменений...]
}
// [обработка ошибок без изменений...]
}
```

## 4. Обновить функцию поиска файлов для INU:

```javascript
async function findFileInArchiveByBlobPath(blobPath, fileName) {
// Если это INU файл (нет архива), возвращаем null
if (!currentZip) {
console.log('Это INU файл, архив не загружен');
return null;
}

// [существующая логика для ZIP архивов...]
}
```

Теперь программа будет корректно определять тип файла (ZIP или INU) и применять соответствующую логику обработки. Для INU файлов она будет читать XML напрямую, без попыток распаковки как ZIP архива.

Добавлено (2025-10-07, 13:34)
---------------------------------------------
Понял проблему! Нужно вернуть рабочую версию и аккуратно добавить поддержку INU файлов. Давайте откатим изменения и сделаем правильную реализацию.

Вот исправленная версия функции `loadWellData`:

```javascript
async function loadWellData(file, fileNumber) {
// Запускаем keep-alive при начале загрузки
startKeepAlive();

showLoading('Анализ архива...');
const wellDataElement = document.getElementById('wellData');
wellDataElement.innerHTML = '<div class="loading"><p>Загрузка данных...</p></div>';

try {
console.log('Начало загрузки файла:', file.name);
updateLoadingText('Чтение файла...');
updateProgress(5, 'Подготовка...');

const fileObj = await file.handle.getFile();
console.log('Размер файла:', fileObj.size, 'байт');

// ПРОСТАЯ ПРОВЕРКА ТИПА ФАЙЛА
const fileName = fileObj.name.toLowerCase();
const isInuFile = fileName.endsWith('.inu');

if (isInuFile) {
// ОБРАБОТКА INU ФАЙЛА
console.log('Обработка INU файла');
updateLoadingText('Чтение INU файла...');
updateProgress(30, 'Чтение XML данных...');

// Читаем содержимое INU файла как текст
const xmlContent = await fileObj.text();
console.log('INU файл прочитан, размер:', xmlContent.length, 'символов');

// Определяем тип файла по имени
const fileType = getFileType(fileObj.name);
console.log('Тип INU файла:', fileType);

updateProgress(50, 'Анализ данных скважины...');

// Ищем скважины в INU файле
const wells = detectWellsInContent(xmlContent, fileObj.name);
console.log('Найдено скважин в INU файле:', wells.length);

if (wells.length === 0) {
throw new Error('В INU файле не найдено данных скважин');
}

// Сохраняем XML контент для быстрого доступа
fileCache.set(fileObj.name, xmlContent);

// Для INU файлов создаем виртуальную структуру
currentZip = {
files: {
// Создаем виртуальную запись файла для совместимости
[fileObj.name]: {
async: function(type) {
return Promise.resolve(new TextEncoder().encode(xmlContent));
},
name: fileObj.name,
dir: false
}
}
};

// ОБРАБОТКА СКВАЖИН (одинаковая для ZIP и INU)
if (wells.length === 1) {
// Одна скважина - сразу загружаем и отображаем
const well = wells[0];
console.log(`Одна скважина найдена: ${well.wellId}, загружаем напрямую`);

updateProgress(70, 'Загрузка данных скважины...');
await parseWellDataUniversal(xmlContent, well.wellId, fileType);

updateProgress(100, 'Готово!');
setTimeout(() => {
hideLoading();
stopKeepAlive();
displayWellData(fileType, fileNumber, file.name, well.wellId);
}, 100);

} else {
// Несколько скважин - показываем список
console.log(`Найдено ${wells.length} скважин, показываем список`);

currentMultiWellData = {
wells: wells,
fileNames: [fileObj.name],
fileType: fileType,
xmlContent: xmlContent // Сохраняем XML контент
};
currentMultiFileName = file.name;

updateProgress(100, 'Готово!');
setTimeout(() => {
hideLoading();
stopKeepAlive();
showWellsList(wells, file.name, 1);
updateWellHeaderForMultiFile(fileType, file.name);
wellDataElement.innerHTML = '<div class="loading"><p>Выберите скважину из списка</p></div>';
}, 100);
}

} else {
// ОБРАБОТКА ZIP АРХИВА (оригинальная логика)
if (fileObj.size > 1500 * 1024 * 1024) {
throw new Error('Файл слишком большой для обработки в браузере (максимум 1500 МБ)');
}

updateLoadingText('Распаковка архива...');
updateProgress(15, 'Распаковка архива...');

const arrayBuffer = await Promise.race([
fileObj.arrayBuffer(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Таймаут чтения файла')), 30000)
)
]);

console.log('ArrayBuffer прочитан, размер:', arrayBuffer.byteLength);

updateLoadingText('Обработка данных...');
updateProgress(25, 'Обработка ZIP...');

const zip = await Promise.race([
JSZip.loadAsync(arrayBuffer),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Таймаут распаковки архива')), 45000)
)
]);

console.log('ZIP распакован, файлов:', Object.keys(zip.files).length);

currentZip = zip;

// Получаем файлы с корректными именами
const allFiles = getZipFileNames(zip);

// Фильтруем только INU файлы
const inuFiles = allFiles.filter(file =>
file.correctedName.startsWith('904~') ||
file.correctedName.startsWith('906~') ||
file.correctedName.startsWith('911~') ||
file.correctedName.startsWith('909~')
);

if (inuFiles.length === 0) {
throw new Error('В архиве не найдено INU файлов с данными');
}

console.log('Найдены INU файлы:', inuFiles.map(f => f.correctedName));
updateProgress(30, `Найдено ${inuFiles.length} файлов`);

// Получаем типы файлов для отображения
const fileTypes = [...new Set(inuFiles.map(f => getFileType(f.correctedName)))];
const fileTypeText = fileTypes.map(type => {
const typeNames = {
'multi': 'Выгрузка для ПС',
'all': 'Все типы документаций',
'final': 'Итоговая документация',
'primary': 'Первичная документация'
};
return typeNames[type] || type;
}).join(', ');

// ОБНОВЛЯЕМ ЗАГОЛОВОК СРАЗУ ПРИ ЗАГРУЗКЕ АРХИВА
updateWellHeaderOnArchiveLoad(fileTypeText, file.name);

// Запускаем долгосрочный прогресс для обработки файлов
const progressInterval = startLongOperationProgress('Обработка файлов');

// УНИВЕРСАЛЬНЫЙ ПОИСК СКВАЖИН ВО ВСЕХ ФАЙЛАХ
const allWells = [];
let processedFiles = 0;

for (const inuFile of inuFiles) {
try {
// Обновляем прогресс для каждого файла
processedFiles++;
const fileProgress = 30 + (processedFiles / inuFiles.length) * 40;
updateProgress(fileProgress, `Обработка файлов: ${processedFiles}/${inuFiles.length}`);

console.log('Обработка файла:', inuFile.correctedName);

// Добавляем таймаут для чтения каждого файла
const xmlContent = await Promise.race([
readZipFileFast(inuFile.entry),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Таймаут чтения файла ${inuFile.correctedName}`)), 15000)
)
]);

const wells = detectWellsInContent(xmlContent, inuFile.correctedName);

console.log(`В файле найдено скважин:`, wells.length);

// Сохраняем XML контент для быстрого доступа
wells.forEach(well => {
well.sourceFile = inuFile.correctedName;
well.originalSourceFile = inuFile.originalName;
well.xmlContent = xmlContent;
well.fileType = getFileType(inuFile.correctedName);
});

allWells.push(...wells);

// Сохраняем в кеш файлов по оригинальному имени
fileCache.set(inuFile.originalName, xmlContent);

} catch (error) {
console.error(`Ошибка обработки файла:`, error);
// Продолжаем обработку других файлов даже при ошибке
}
}

// Останавливаем долгосрочный прогресс
if (progressInterval) {
clearInterval(progressInterval);
}

if (allWells.length === 0) {
throw new Error('В архиве не найдено данных скважин');
}

// ВАЖНО: Убираем дубликаты скважин по wellId
const uniqueWells = [];
const seenWellIds = new Set();

for (const well of allWells) {
if (!seenWellIds.has(well.wellId)) {
seenWellIds.add(well.wellId);
uniqueWells.push(well);
}
}

console.log(`Уникальных скважин: ${uniqueWells.length} (всего найдено: ${allWells.length})`);
updateProgress(80, `Найдено ${uniqueWells.length} скважин`);

// ЕДИНАЯ ЛОГИКА ДЛЯ ВСЕХ СЛУЧАЕВ
if (uniqueWells.length === 1) {
// Одна скважина - сразу загружаем и отображаем
const well = uniqueWells[0];
console.log(`Одна скважина найдена: ${well.wellId}, загружаем напрямую`);

updateProgress(90, 'Загрузка данных скважины...');
await parseWellDataUniversal(well.xmlContent, well.wellId, well.fileType);

updateProgress(100, 'Готово!');
setTimeout(() => {
hideLoading();
stopKeepAlive();
displayWellData(well.fileType, fileNumber, file.name, well.wellId);
}, 100);

} else {
// Несколько скважин - показываем список
console.log(`Найдено ${uniqueWells.length} скважин, показываем список`);

currentMultiWellData = {
wells: uniqueWells,
fileNames: [...new Set(uniqueWells.map(w => w.sourceFile))],
fileType: fileTypes.length === 1 ? fileTypes[0] : 'mixed'
};
currentMultiFileName = file.name;

updateProgress(100, 'Готово!');
setTimeout(() => {
hideLoading();
stopKeepAlive();
showWellsList(uniqueWells, file.name, currentMultiWellData.fileNames.length);
updateWellHeaderForMultiFile(currentMultiWellData.fileType, file.name);
wellDataElement.innerHTML = '<div class="loading"><p>Выберите скважину из списка</p></div>';
}, 100);
}
}

} catch (error) {
// Гарантированно останавливаем keep-alive при ошибке
stopKeepAlive();
hideLoading();

console.error('Ошибка загрузки данных:', error);
wellDataElement.innerHTML = `
<div class="error">
<p><strong>Ошибка загрузки данных:</strong> ${error.message}</p>
<p>Попробуйте выбрать другой файл или архив</p>
${error.message.includes('Таймаут') ? `
<div style="margin-top: 15px; padding: 10px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px;">
<strong>Совет:</strong> Попробуйте выбрать меньший файл или проверьте стабильность интернет-соединения
</div>
` : ''}
</div>
`;
}
}
```

Ключевые изменения:

1. **Простая проверка типа файла** - только по расширению `.inu`
2. **Для INU файлов** - читаем напрямую как текст и создаем виртуальную структуру для совместимости
3. **Для ZIP архивов** - оставляем оригинальную логику без изменений
4. **Общая обработка скважин** - одинаковый код для обоих типов файлов после получения XML

Теперь должно работать корректно для обоих типов файлов!

Добавлено (2025-10-07, 13:43)
---------------------------------------------
Понял проблему! Ошибка возникает потому что для INU файлов не создаются карты данных (classifierMap, employeeMap и т.д.). Нужно исправить функцию `parseWellDataUniversal` чтобы она работала для INU файлов.

Вот исправления:

## 1. Исправить функцию `parseWellDataUniversal`:

```javascript
async function parseWellDataUniversal(xmlContent, wellId, fileType, signal = null) {
// Проверяем кеш
const cachedData = getCachedWellData(wellId);
if (cachedData) {
console.log('Используем кешированные данные для скважины:', wellId);

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

// НЕМЕДЛЕННО устанавливаем данные и скрываем загрузку
xmlData = cachedData;

// Скрываем загрузку и возвращаем данные
setTimeout(() => {
hideLoading();
}, 50);

return cachedData;
}

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

// Запускаем keep-alive для долгих парсингов
startKeepAlive();

showLoading('Загрузка данных скважины...');

try {
updateProgress(5, 'Подготовка данных...');

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

// Имитируем прогресс для начальной обработки
await new Promise(resolve => setTimeout(resolve, 100));
updateProgress(15, 'Анализ структуры данных...');

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

updateProgress(25, 'Создание карт данных...');

// ВАЖНО: Проверяем что xmlContent не пустой и корректен
if (!xmlContent || xmlContent.length < 100) {
throw new Error('XML данные пустые или повреждены');
}

console.log('Создание карт данных для XML длиной:', xmlContent.length);

// Создаем карты данных с небольшими задержками для прогресса
await new Promise(resolve => setTimeout(resolve, 50));
const classifierMap = createClassifierMap(xmlContent);
console.log('Карта классификаторов создана, размер:', classifierMap.size);
updateProgress(35, 'Карта классификаторов создана');

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

await new Promise(resolve => setTimeout(resolve, 50));
const employeeMap = createEmployeeMap(xmlContent);
console.log('Карта сотрудников создана, размер:', employeeMap.size);
updateProgress(45, 'Карта сотрудников создана');

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

await new Promise(resolve => setTimeout(resolve, 50));
const assayTypeMap = createAssayTypeMap(xmlContent);
console.log('Карта типов опробования создана, размер:', assayTypeMap.size);
updateProgress(55, 'Карта типов опробования создана');

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

await new Promise(resolve => setTimeout(resolve, 50));
const divisionMap = createDivisionMap(xmlContent);
console.log('Карта подразделений создана, размер:', divisionMap.size);
updateProgress(65, 'Карта подразделений создана');

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

updateProgress(75, 'Извлечение основной информации о скважине...');

// Используем существующий быстрый парсер для всех типов файлов
console.time(`Универсальный парсинг для ${wellId}`);

// Добавляем таймаут для парсинга
const wellData = await Promise.race([
parseAllWellDataFast(xmlContent, wellId, classifierMap, employeeMap, assayTypeMap, divisionMap),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Таймаут парсинга данных скважины')), 60000)
)
]);

console.timeEnd(`Универсальный парсинг для ${wellId}`);

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

if (wellData) {
updateProgress(85, 'Обработка интервалов документирования...');
await new Promise(resolve => setTimeout(resolve, 50));

updateProgress(90, 'Обработка данных опробования...');
await new Promise(resolve => setTimeout(resolve, 50));

// Добавляем информацию о типе файла
wellData.fileType = fileType;
wellData.wellId = wellId;

xmlData = wellData;

updateProgress(95, 'Сохранение в кеш...');
await new Promise(resolve => setTimeout(resolve, 30));

// Сохраняем в кеш
setCachedWellData(wellId, wellData);

updateProgress(100, 'Данные скважины успешно загружены!');

// Проверяем отмену операции перед завершением
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

setTimeout(() => {
hideLoading();
stopKeepAlive(); // Останавливаем keep-alive
}, 200);

return wellData;
}

throw new Error('Не удалось извлечь данные скважины');

} catch (error) {
// Игнорируем ошибки отмены
if (error.name === 'AbortError') {
console.log('Парсинг скважины отменен');
throw error;
}

// Гарантированно останавливаем keep-alive при ошибке
stopKeepAlive();
hideLoading();

console.error('Ошибка универсального парсинга скважины:', error);
console.error('XML content length:', xmlContent ? xmlContent.length : 'null');
console.error('WellId:', wellId);
console.error('FileType:', fileType);

// Очищаем кеш для этой скважины при ошибке
wellDataCache.delete(wellId);

// Показываем информативное сообщение об ошибке
let errorMessage = error.message;
if (error.message.includes('Таймаут')) {
errorMessage = 'Превышено время обработки данных. Попробуйте выбрать другую скважину или файл меньшего размера.';
} else if (error.message.includes('пустые') || error.message.includes('повреждены')) {
errorMessage = 'Данные файла повреждены или имеют неверный формат.';
}

throw new Error(errorMessage);
}
}
```

## 2. Обновить функцию `parseAllWellDataFast`:

```javascript
async function parseAllWellDataFast(xmlContent, wellId, classifierMap, employeeMap, assayTypeMap, divisionMap) {
return new Promise((resolve) => {
try {
console.time(`Fast parse ALL data for ${wellId}`);

console.log('Парсинг данных с картами:');
console.log('- classifierMap:', classifierMap.size);
console.log('- employeeMap:', employeeMap.size);
console.log('- assayTypeMap:', assayTypeMap.size);
console.log('- divisionMap:', divisionMap.size);

// [остальной код функции остается без изменений]
// просто используем переданные карты вместо создания новых
```

## 3. Обновить вызов в `loadWellData` для INU файлов:

Найти в функции `loadWellData` блок для INU файлов где вызывается `parseWellDataUniversal` и убедиться что передается правильный xmlContent:

```javascript
// В блоке обработки INU файлов:
await parseWellDataUniversal(xmlContent, well.wellId, fileType);
```

## 4. Добавить защиту в функции создания карт:

```javascript
function createClassifierMap(xmlContent) {
const classifierMap = new Map();
try {
// Проверяем что xmlContent существует
if (!xmlContent || typeof xmlContent !== 'string') {
console.warn('Некорректный xmlContent для создания карты классификаторов');
return classifierMap;
}

const classifierRegex = /<MR_DBA\.CLASSIFIER>[\s\S]*?<\/MR_DBA\.CLASSIFIER>/g;
const classifierMatches = xmlContent.match(classifierRegex) || [];

console.log('Найдено CLASSIFIER блоков:', classifierMatches.length);

for (const match of classifierMatches) {
const nodeId = extractValue(match, 'ID_NODE_ID');
const nodeName = extractValue(match, 'NODE_NAME');
if (nodeId && nodeName) {
classifierMap.set(nodeId, nodeName);
}
}
} catch (error) {
console.error('Ошибка создания карты классификаторов:', error);
}
return classifierMap;
}
```

Аналогично добавить проверки в другие функции создания карт (`createEmployeeMap`, `createAssayTypeMap`, `createDivisionMap`).

Теперь карты данных будут создаваться корректно для INU файлов, и ошибка "создания карт" должна исчезнуть!

Добавлено (2025-10-07, 13:50)
---------------------------------------------
Понял! Ошибка возникает потому что для мульти-файлов INU данные передаются неправильно. Давайте исправим логику передачи XML контента.

## 1. Исправить функцию `parseWellDataUniversal` - убрать строгую проверку:

```javascript
async function parseWellDataUniversal(xmlContent, wellId, fileType, signal = null) {
// Проверяем кеш
const cachedData = getCachedWellData(wellId);
if (cachedData) {
console.log('Используем кешированные данные для скважины:', wellId);

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

xmlData = cachedData;

setTimeout(() => {
hideLoading();
}, 50);

return cachedData;
}

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

// Запускаем keep-alive для долгих парсингов
startKeepAlive();

showLoading('Загрузка данных скважины...');

try {
updateProgress(5, 'Подготовка данных...');

// ПРОВЕРКА XML ДАННЫХ - БОЛЕЕ ГИБКАЯ
if (!xmlContent) {
throw new Error('XML данные отсутствуют');
}

if (xmlContent.length < 50) {
console.warn('XML данные очень короткие, но пробуем обработать:', xmlContent.length);
}

console.log('Начало парсинга XML длиной:', xmlContent.length, 'символов');

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

await new Promise(resolve => setTimeout(resolve, 100));
updateProgress(15, 'Анализ структуры данных...');

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

updateProgress(25, 'Создание карт данных...');

console.log('Создание карт данных...');

// Создаем карты данных с защитой от ошибок
await new Promise(resolve => setTimeout(resolve, 50));
const classifierMap = createClassifierMap(xmlContent);
console.log('Карта классификаторов создана, размер:', classifierMap.size);
updateProgress(35, 'Карта классификаторов создана');

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

await new Promise(resolve => setTimeout(resolve, 50));
const employeeMap = createEmployeeMap(xmlContent);
console.log('Карта сотрудников создана, размер:', employeeMap.size);
updateProgress(45, 'Карта сотрудников создана');

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

await new Promise(resolve => setTimeout(resolve, 50));
const assayTypeMap = createAssayTypeMap(xmlContent);
console.log('Карта типов опробования создана, размер:', assayTypeMap.size);
updateProgress(55, 'Карта типов опробования создана');

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

await new Promise(resolve => setTimeout(resolve, 50));
const divisionMap = createDivisionMap(xmlContent);
console.log('Карта подразделений создана, размер:', divisionMap.size);
updateProgress(65, 'Карта подразделений создана');

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

updateProgress(75, 'Извлечение основной информации о скважине...');

// Используем существующий быстрый парсер для всех типов файлов
console.time(`Универсальный парсинг для ${wellId}`);

// Передаем созданные карты в парсер
const wellData = await parseAllWellDataFast(xmlContent, wellId, classifierMap, employeeMap, assayTypeMap, divisionMap);

console.timeEnd(`Универсальный парсинг для ${wellId}`);

// Проверяем отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

if (wellData) {
updateProgress(85, 'Обработка интервалов документирования...');
await new Promise(resolve => setTimeout(resolve, 50));

updateProgress(90, 'Обработка данных опробования...');
await new Promise(resolve => setTimeout(resolve, 50));

// Добавляем информацию о типе файла
wellData.fileType = fileType;
wellData.wellId = wellId;

xmlData = wellData;

updateProgress(95, 'Сохранение в кеш...');
await new Promise(resolve => setTimeout(resolve, 30));

// Сохраняем в кеш
setCachedWellData(wellId, wellData);

updateProgress(100, 'Данные скважины успешно загружены!');

// Проверяем отмену операции перед завершением
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

setTimeout(() => {
hideLoading();
stopKeepAlive();
}, 200);

return wellData;
}

throw new Error('Не удалось извлечь данные скважины');

} catch (error) {
// Игнорируем ошибки отмены
if (error.name === 'AbortError') {
console.log('Парсинг скважины отменен');
throw error;
}

// Гарантированно останавливаем keep-alive при ошибке
stopKeepAlive();
hideLoading();

console.error('Ошибка универсального парсинга скважины:', error);
console.error('XML content length:', xmlContent ? xmlContent.length : 'null');
console.error('WellId:', wellId);
console.error('FileType:', fileType);

// Очищаем кеш для этой скважины при ошибке
wellDataCache.delete(wellId);

throw error; // Пробрасываем оригинальную ошибку
}
}
```

## 2. Исправить функцию `parseWellDataFromMulti` для INU файлов:

```javascript
async function parseWellDataFromMulti(wellId, signal = null) {
// ПРОВЕРЯЕМ отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

if (!currentMultiWellData || !currentMultiWellData.wells) {
throw new Error('Данные мульти-файла не загружены');
}

// ПРОВЕРЯЕМ КЕШ с защитой от ошибок
const cachedData = getCachedWellData(wellId);
if (cachedData) {
console.log('Используем кешированные данные для скважины:', wellId);

// ПРОВЕРЯЕМ отмену операции перед возвратом данных
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

xmlData = cachedData;
return;
}

showLoading('Загрузка данных скважины...');

try {
updateProgress(10, 'Поиск информации о скважине...');

// ПРОВЕРЯЕМ отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

const targetWellMeta = currentMultiWellData.wells.find(w => w.wellId === wellId);
if (!targetWellMeta) {
throw new Error(`Скважина с ID ${wellId} не найдена в списке`);
}

let xmlContent = '';
let sourceFile = '';

// Случай 1: ZIP архив (данные в fileCache)
if (targetWellMeta.sourceFile && fileCache.has(targetWellMeta.sourceFile)) {
sourceFile = targetWellMeta.sourceFile;
updateProgress(30, 'Загрузка данных из кеша...');

// ПРОВЕРЯЕМ отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

xmlContent = fileCache.get(sourceFile);
if (!xmlContent) {
throw new Error(`Файл ${sourceFile} не найден в кеше`);
}
}
// Случай 2: INU файл (данные в currentMultiWellData.xmlContent)
else if (currentMultiWellData.xmlContent) {
updateProgress(30, 'Загрузка данных из INU файла...');
xmlContent = currentMultiWellData.xmlContent;
sourceFile = 'current_inu_file';
console.log('Используем XML из currentMultiWellData, длина:', xmlContent.length);
}
// Случай 3: Данные в самом объекте скважины
else if (targetWellMeta.xmlContent) {
updateProgress(30, 'Загрузка данных из объекта скважины...');
xmlContent = targetWellMeta.xmlContent;
sourceFile = 'well_object';
console.log('Используем XML из targetWellMeta, длина:', xmlContent.length);
}
// Случай 4: Не удалось определить источник
else {
throw new Error('Не удалось определить источник данных для скважины');
}

// ВАЖНО: Проверяем что xmlContent не пустой
if (!xmlContent || xmlContent.length < 10) {
throw new Error('XML данные пустые или отсутствуют для скважины ' + wellId);
}

updateProgress(50, 'Быстрая обработка всех данных...');

// ПРОВЕРЯЕМ отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

// ИСПОЛЬЗУЕМ БЫСТРЫЙ МЕТОД ДЛЯ ВСЕХ ДАННЫХ
console.time(`Parse ALL data for ${wellId}`);
const allWellData = await parseAllWellDataFast(xmlContent, wellId);
console.timeEnd(`Parse ALL data for ${wellId}`);

// ПРОВЕРЯЕМ отмену операции
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

if (allWellData) {
// ВАЖНО: Сохраняем wellId в данных для корректного отображения
allWellData.wellId = wellId;

xmlData = allWellData;

// Сохраняем в кеш с защитой от ошибок
setCachedWellData(wellId, xmlData);

updateProgress(100, 'Готово!');

// ПРОВЕРЯЕМ отмену операции перед завершением
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

setTimeout(() => {
hideLoading();
}, 50);
return;
}

throw new Error('Не удалось извлечь данные скважины');

} catch (error) {
// Игнорируем ошибки отмены
if (error.name === 'AbortError') {
throw error;
}

hideLoading();
console.error('Ошибка загрузки данных скважины:', error);

// Очищаем кеш для этой скважины при ошибке
try {
wellDataCache.delete(wellId);
} catch (e) {
console.error('Ошибка очистки кеша:', e);
}

throw error;
}
}
```

## 3. Добавить отладочную информацию в `selectWell`:

```javascript
function selectWell(wellId, wellNumber) {
// Запоминаем позицию прокрутки
const listsContainer = document.getElementById('listsContainer');
if (listsContainer) {
lastScrollPosition = listsContainer.scrollTop;
}

// Скрываем меню в мобильной версии
if (window.innerWidth <= 1024) {
const sidebar = document.querySelector('.sidebar');
const overlay = document.getElementById('overlay');
if (sidebar) sidebar.classList.remove('active');
if (overlay) overlay.classList.remove('active');
}

// Сброс активного класса у всех элементов
document.querySelectorAll('.well-item').forEach(item => {
item.classList.remove('active');
});

// Добавление активного класса к выбранному элементу
event.currentTarget.classList.add('active');

currentWellId = wellId;

// Находим данные скважины
const wellMeta = currentMultiWellData.wells.find(w => w.wellId === wellId);
if (!wellMeta) {
console.error('Данные скважины не найдены:', wellId);
return;
}

console.log('Выбрана скважина:', wellId);
console.log('Данные скважины:', wellMeta);
console.log('currentMultiWellData:', currentMultiWellData);

abortController = new AbortController();
currentLoadingOperation = { wellId, timestamp: Date.now() };

showLoading('Загрузка данных скважины...');

// Используем setTimeout для асинхронной обработки
setTimeout(async () => {
// Проверяем, не была ли операция отменена
if (!currentLoadingOperation || currentLoadingOperation.wellId !== wellId) {
return;
}

try {
// Используем универсальный парсер
await parseWellDataFromMulti(wellId, abortController.signal);

// ВАЖНО: После успешной загрузки ОБЯЗАТЕЛЬНО отображаем данные
displayWellData(wellMeta.fileType, wellNumber, currentMultiFileName, wellId);

// Очищаем операцию после успешной загрузки
if (currentLoadingOperation && currentLoadingOperation.wellId === wellId) {
currentLoadingOperation = null;
abortController = null;
}
} catch (error) {
// Игнорируем ошибки отмены
if (error.name === 'AbortError') {
console.log('Загрузка отменена пользователем');
return;
}

console.error('Ошибка загрузки скважины:', error);

// Сбрасываем активный класс при ошибке
event.currentTarget.classList.remove('active');

// Очищаем операцию при ошибке
currentLoadingOperation = null;
abortController = null;

document.getElementById('wellData').innerHTML = `
<div class="error">
<p><strong>Ошибка загрузки данных скважины:</strong> ${error.message}</p>
<p>Попробуйте выбрать другую скважину</p>
</div>
`;
hideLoading();
}
}, 50);
}
```

Теперь XML данные должны передаваться корректно для INU файлов!

Добавлено (2025-10-07, 13:58)
---------------------------------------------
Отлично! Удалим функцию открытия отдельных INU файлов и заменим её на открытие ZIP архивов. Вот изменения:

## 1. Удаляем кнопку "Открыть INU файл" из HTML:

```javascript
// В разделе actions-container заменяем:
<div class="main-actions">
<button class="action-btn primary" id="selectFolderBtn">
<span class="btn-icon">📁</span>
<span class="btn-text">Выбрать папку</span>
</button>
<button class="action-btn secondary" id="selectFileBtn">
<span class="btn-icon">📄</span>
<span class="btn-text">Открыть ZIP</span> <!-- ← ИЗМЕНИЛИ ТЕКСТ -->
</button>
</div>
```

## 2. Обновляем функцию выбора файла:

```javascript
// Удаляем старую функцию selectFileManually и заменяем на:
async function selectZipFile() {
try {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.zip'; // ← ТОЛЬКО ZIP ФАЙЛЫ
input.onchange = async (e) => {
const file = e.target.files[0];
if (file) {
// ОЧИСТКА СПИСКА ПРИ ВЫБОРЕ НОВОГО ФАЙЛА
clearFileList();
clearCaches();

// Создаем временный file handle-like объект
const tempFile = {
name: file.name,
handle: { getFile: () => Promise.resolve(file) }
};

// Обновляем отображение пути
document.getElementById('folderPath').innerHTML = `
<div class="folder-path-content">
<span class="folder-icon">📦</span>
<span class="folder-text">${file.name}</span>
</div>
`;

await loadWellData(tempFile, 0);
}
};
input.click();
} catch (error) {
console.error('Ошибка выбора ZIP файла:', error);
alert('Ошибка при выборе ZIP файла: ' + error.message);
}
}
```

## 3. Обновляем обработчики в DOMContentLoaded:

```javascript
// Заменяем обработчики:
document.getElementById('selectFolderBtn').addEventListener('click', selectFolder);
document.getElementById('selectFileBtn').addEventListener('click', selectZipFile); // ← ИЗМЕНИЛИ ФУНКЦИЮ
```

## 4. Обновляем функцию clearFileList для случая с ZIP:

```javascript
function clearFileList() {
const fileList = document.getElementById('fileList');
const wellsList = document.getElementById('wellsList');
const backButton = document.getElementById('backButton');

if (fileList) fileList.innerHTML = '<li class="file-item">Выберите папку или ZIP архив</li>'; // ← ОБНОВИЛИ ТЕКСТ
if (wellsList) {
wellsList.innerHTML = '';
wellsList.style.display = 'none';
}
if (backButton) backButton.style.display = 'none';

// Сбрасываем глобальные переменные
currentMultiWellData = null;
currentWellId = null;
currentMultiFileName = null;
currentDirectoryHandle = null;

// Обновляем счетчик
document.getElementById('fileCounter').textContent = '';

// Сбрасываем путь к папке
resetFolderPath();
}

function resetFolderPath() {
document.getElementById('folderPath').innerHTML = `
<div class="folder-path-content">
<span class="folder-icon">📂</span>
<span class="folder-text">Выберите папку или ZIP архив</span> <!-- ← ОБНОВИЛИ ТЕКСТ -->
</div>
`;
}
```

## 5. Обновляем тексты интерфейса:

```javascript
// В функции updateSidebarHeader:
function updateSidebarHeader(title) {
const sidebarHeader = document.querySelector('.sidebar-header h2');
if (sidebarHeader) {
sidebarHeader.textContent = title;
}
}

// В начальном состоянии:
document.addEventListener('DOMContentLoaded', function() {
// ... существующий код ...

// Устанавливаем начальный текст
updateSidebarHeader('Список архивов');
document.getElementById('fileList').innerHTML = '<li class="file-item">Выберите папку или ZIP архив</li>';
});
```

## 6. Обновляем тексты в счетчике файлов:

```javascript
// В функции loadFileList обновляем тексты:
const subfolderText = searchSubfolders ? ' (включая подпапки)' : '';
document.getElementById('fileCounter').textContent = `Найдено архивов: ${files.length}${subfolderText}`;

// И при отсутствии файлов:
fileList.innerHTML = '<li class="file-item">В папке нет архивов ZIP</li>';
```

## Полный код изменений:

```javascript
// ЗАМЕНЯЕМ старую функцию selectFileManually на:
async function selectZipFile() {
try {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.zip';
input.onchange = async (e) => {
const file = e.target.files[0];
if (file) {
clearFileList();
clearCaches();

const tempFile = {
name: file.name,
handle: { getFile: () => Promise.resolve(file) }
};

document.getElementById('folderPath').innerHTML = `
<div class="folder-path-content">
<span class="folder-icon">📦</span>
<span class="folder-text">${file.name}</span>
</div>
`;

await loadWellData(tempFile, 0);
}
};
input.click();
} catch (error) {
console.error('Ошибка выбора ZIP файла:', error);
alert('Ошибка при выборе ZIP файла: ' + error.message);
}
}

// ОБНОВЛЯЕМ обработчики в DOMContentLoaded:
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('selectFolderBtn').addEventListener('click', selectFolder);
document.getElementById('selectFileBtn').addEventListener('click', selectZipFile); // ← ИЗМЕНИЛИ
// ... остальной код ...
});
```

Теперь приложение будет работать только с папками и отдельными ZIP архивами, что упростит интерфейс и логику работы.

Добавлено (2025-10-07, 14:23)
---------------------------------------------
Отлично! Добавим кнопки выгрузки в Excel для всех таблиц. Вот реализация:

## 1. Добавляем библиотеку для работы с Excel:

```javascript
// Добавляем в head или перед закрывающим тегом body
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
```

## 2. Создаем функцию для экспорта в Excel:

```javascript
// Функция для экспорта таблицы в Excel
function exportTableToExcel(tableElement, fileName) {
try {
// Создаем новую книгу Excel
const wb = XLSX.utils.book_new();

// Получаем данные из таблицы
const table = tableElement.querySelector('table');
const worksheet = XLSX.utils.table_to_sheet(table);

// Добавляем лист в книгу
XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Сохраняем файл
XLSX.writeFile(wb, `${fileName}_${new Date().toISOString().split('T')[0]}.xlsx`);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');
} catch (error) {
console.error('Ошибка экспорта в Excel:', error);
showNotification('Ошибка при экспорте таблицы', 'error');
}
}

// Функция создания кнопки экспорта
function createExportButton(tableId, fileName) {
return `
<div class="export-section" style="margin-bottom: 15px; display: flex; justify-content: flex-end;">
<button class="export-excel-btn"
data-table-id="${tableId}"
data-file-name="${fileName}"
style="background: linear-gradient(135deg, #27ae60 0%, #219653 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 4px rgba(39, 174, 96, 0.3);
transition: all 0.2s ease;">
<span style="font-size: 1.1rem;">📊</span>
<span>Экспорт в Excel</span>
</button>
</div>
`;
}
```

## 3. Обновляем функции отображения таблиц с кнопками экспорта:

### Для первичной документации:
```javascript
function displayPrimaryDocumentation() {
let html = '';

// Информация о первичном документировании
if (xmlData.primaryDocInfo) {
// ... существующий код ...
}

// Добавляем кнопку экспорта
html += createExportButton('primaryDocTable', 'Первичное_документирование');

html += `
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table" id="primaryDocTable">
<thead>
<tr>
<th style="width: 50px;">ПП</th>
<th style="width: 80px;">От (м)</th>
<th style="width: 80px;">До (м)</th>
<th style="width: 100px;">Мощность (м)</th>
<th style="width: 150px;">Стратиграфия</th>
<th style="width: 150px;">Литология</th>
<th>Описание</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>`;

xmlData.primaryDocumentation.forEach(interval => {
html += `
<tr>
<td style="width: 50px;">${interval.numberPP}</td>
<td style="width: 80px;">${formatNumber(interval.from)}</td>
<td style="width: 80px;">${formatNumber(interval.to)}</td>
<td style="width: 100px;">${formatNumber(interval.thickness)}</td>
<td style="width: 150px;" class="stratigraphy-column">${interval.stratigraphy}</td>
<td style="width: 150px;">${interval.lithology}</td>
<td><div class="description-text">${interval.description}</div></td>
</tr>`;
});

html += `
</tbody>
</table>
</div>
</div>
`;

return html;
}
```

### Для итоговой документации:
```javascript
function displayFinalDocumentation() {
let html = '';

// ... существующий код информации о документировании ...

// Добавляем кнопку экспорта
html += createExportButton('finalDocTable', 'Итоговое_документирование');

html += `
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table" id="finalDocTable">
<thead>
<tr>
<th style="width: 50px;">ПП</th>
<th style="width: 80px;">От (м)</th>
<th style="width: 80px;">До (м)</th>
<th style="width: 100px;">Мощность (м)</th>
<th style="width: 150px;">Стратиграфия</th>
<th style="width: 150px;">Литология</th>
<th>Описание</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>`;

// ... существующий код таблицы ...

return html;
}
```

### Для ГИС документации:
```javascript
function displayGisDocumentation() {
let html = '';

// ... существующий код ...

if (xmlData.gisDocumentation.length > 0) {
// Добавляем кнопку экспорта
html += createExportButton('gisDocTable', 'ГИС_документирование');

html += `
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table" id="gisDocTable">
<thead>
<tr>
<th style="width: 50px;">ПП</th>
<th style="width: 80px;">От (м)</th>
<th style="width: 80px;">До (м)</th>
<th style="width: 100px;">Мощность (м)</th>
<th style="width: 150px;">Стратиграфия</th>
<th style="width: 150px;">Литология</th>
<th>Описание</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>`;

// ... заполнение таблицы ...
}

return html;
}
```

### Для опробования:
```javascript
function displayAssays() {
let html = '';

if (xmlData.assays.length > 0) {
// Добавляем кнопку экспорта
html += createExportButton('assaysTable', 'Опробование');

html += `
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table" id="assaysTable">
<thead>
<tr>
<th style="width: 60px;">№ пробы</th>
<th style="width: 120px;">Тип опробования</th>
<th style="width: 80px;">От (м)</th>
<th style="width: 80px;">До (м)</th>
<th style="width: 100px;">Значение</th>
<th style="width: 80px;">Ед. изм.</th>
<th style="width: 120px;">Штрих-код</th>
<th style="width: 120px;">Место отбора</th>
<th style="width: 120px;">Автор отбора</th>
<th style="width: 100px;">Дата отбора</th>
<th>Описание</th>
<th style="width: 150px;">Тип документирования</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>`;

// ... заполнение таблицы ...
}

return html;
}
```

### Для керновых ящиков:
```javascript
function displayCoreBoxes(coreBoxes) {
if (!coreBoxes || coreBoxes.length === 0) {
return '<div class="no-data"><p>Нет данных о керновых ящиках</p></div>';
}

let html = `
<div class="doc-info-list" style="margin-bottom: 20px;">
<div class="doc-info-item">
<span class="doc-info-label">Всего керновых ящиков:</span>
<span class="doc-info-value"><strong>${coreBoxes.length}</strong></span>
</div>
</div>
`;

// Добавляем кнопку экспорта
html += createExportButton('coreBoxesTable', 'Керновые_ящики');

html += `
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table" id="coreBoxesTable">
<thead>
<tr>
<th style="width: 80px;">№ ящика</th>
<th style="width: 100px;">От (м)</th>
<th style="width: 100px;">До (м)</th>
<th style="width: 100px;">Длина (м)</th>
<th style="width: 120px;">Базальный горизонт</th>
<th style="width: 200px;">Описание</th>
<th style="width: 250px;">Связанные документы</th>
<th style="width: 100px;">Дата добавления</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>
`;

// ... заполнение таблицы ...

return html;
}
```

### Для вложенных документов:
```javascript
// В функции displayAttachedDocuments добавляем после фильтра:
html += createExportButton('documentsTable', 'Вложенные_документы');

// И обновляем ID таблицы:
html += `
<div id="documentsTableSection">
<div class="table-wrapper" id="documentsTableWrapper">
<div class="table-header">
<table class="interval-table" id="documentsTable" style="min-width: 1600px;">
<!-- ... остальная таблица ... -->
`;
```

## 4. Добавляем обработчики для кнопок экспорта:

```javascript
// Функция для инициализации обработчиков экспорта
function initializeExportButtons() {
document.querySelectorAll('.export-excel-btn').forEach(button => {
button.addEventListener('click', function() {
const tableId = this.getAttribute('data-table-id');
const fileName = this.getAttribute('data-file-name');
const tableElement = document.getElementById(tableId);

if (tableElement) {
exportTableToExcel(tableElement, fileName);
} else {
console.error('Таблица не найдена:', tableId);
showNotification('Ошибка: таблица не найдена', 'error');
}
});

// Добавляем стили при наведении
button.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)';
});

button.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)';
});
});
}
```

## 5. Обновляем функцию displayWellData для инициализации кнопок:

```javascript
// В функции displayWellData, после инициализации document buttons:
setTimeout(() => {
// ... существующие обработчики ...

// ОБЩИЕ обработчики для кнопок документов
initializeDocumentButtons();

// ОБРАБОТЧИК ДЛЯ ФИЛЬТРА ДОКУМЕНТОВ
const filterCheckbox = document.getElementById('filterRelatedDocuments');
if (filterCheckbox) {
filterCheckbox.addEventListener('change', function() {
filterDocumentsByStatus(this.checked);
});

// Применяем фильтр сразу (по умолчанию включен)
filterDocumentsByStatus(true);
}

// ИНИЦИАЛИЗИРУЕМ КНОПКИ ЭКСПОРТА ← ДОБАВЛЯЕМ ЭТУ СТРОЧКУ
initializeExportButtons();

}, 100);
```

## 6. Добавляем CSS стили для кнопок:

```css
/* Добавляем в CSS секцию */
.export-excel-btn:hover {
background: linear-gradient(135deg, #219653 0%, #1e8449 100%) !important;
transform: translateY(-2px) !important;
box-shadow: 0 4px 8px rgba(39, 174, 96, 0.4) !important;
}

.export-excel-btn:active {
transform: translateY(0) !important;
box-shadow: 0 2px 4px rgba(39, 174, 96, 0.3) !important;
}

.export-section {
margin-bottom: 15px;
display: flex;
justify-content: flex-end;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
```

Теперь над каждой таблицей будет появляться кнопка "Экспорт в Excel", которая выгружает таблицу в форматированный Excel файл с текущей датой в названии.

Добавлено (2025-10-07, 14:44)
---------------------------------------------
Вот полный код функции `displayWellData` с добавленными кнопками экспорта:

```javascript
async function displayWellData(fileType, fileNumber, fileName, wellId = null, signal = null) {
const wellDataElement = document.getElementById('wellData');

// ГАРАНТИРУЕМ что загрузка скрыта перед отображением
hideLoading();

// Проверяем что данные действительно загружены
if (!xmlData) {
console.error('Нет данных для отображения');
wellDataElement.innerHTML = `
<div class="error">
<p>Ошибка: данные не загружены</p>
<p>Попробуйте выбрать скважину еще раз</p>
</div>
`;
return;
}

// Проверяем отмену операции перед отображением
if (signal && signal.aborted) {
throw new DOMException('Operation aborted', 'AbortError');
}

// Обновление заголовка
const badgeClass = fileType === 'all' ? 'all' :
fileType === 'primary' ? 'primary' :
fileType === 'final' ? 'final' :
fileType === 'multi' ? 'multi' : 'unknown';

const badgeText = fileType === 'all' ? 'Все типы документаций' :
fileType === 'primary' ? 'Первичная документация' :
fileType === 'final' ? 'Итоговая документация' :
fileType === 'multi' ? 'Выгрузка для ПС' : 'Неизвестный тип';

document.getElementById('wellName').innerHTML = `
<div class="well-header-content">
<div class="doc-type-badge ${badgeClass}">${badgeText}</div>
<div class="well-title">
${xmlData.lineNumber && xmlData.lineNumber !== 'Не указано' ? `
<div class="well-line-container">
<div class="well-line-label">Линия №:</div>
<div class="well-line-number">${xmlData.lineNumber}</div>
</div>
` : ''}
<div class="well-number-container">
<div class="well-number-label">Номер выработки:</div>
<div class="well-line-number">${xmlData.wellName}</div>
</div>
</div>
${xmlData.wellType && xmlData.wellType !== 'Не указано' ? `
<div class="well-type-inline">
<span class="well-type-label">Тип скважины:</span>
<span class="well-type-value">${xmlData.wellType}</span>
</div>
` : ''}
</div>
`;

// Создание HTML для отображения данных с ИСПРАВЛЕННОЙ СТРУКТУРОЙ ВКЛАДОК
let html = '';

// Основная информация о скважине
html += `
<div class="section">
<h2>Основная информация</h2>

<div class="info-group">
<div class="info-group-title">Объект и участок</div>
<div class="doc-info-list">
<div class="doc-info-item">
<span class="doc-info-label">Объект работ:</span>
<span class="doc-info-value"><strong>${formatLongText(xmlData.object || 'Не указан')}</strong></span>
</div>
<div class="doc-info-item ${!xmlData.area || xmlData.area === 'Не указано' ? 'missing-field-simple' : ''}">
<span class="doc-info-label">Геологический участок:</span>
<span class="doc-info-value">
${xmlData.area && xmlData.area !== 'Не указано' ? `<strong>${formatLongText(xmlData.area)}</strong>` : '<em class="not-specified-text">не указано</em>'}
</span>
</div>
</div>
</div>

<div class="info-group">
<div class="info-group-title">Бурение</div>
<div class="doc-info-list">
<div class="doc-info-item ${!xmlData.drillingStartDate || xmlData.drillingStartDate === '' ? 'missing-field-simple' : ''}">
<span class="doc-info-label">Начало бурения:</span>
<span class="doc-info-value">${xmlData.drillingStartDate || 'Не указано'}</span>
</div>
<div class="doc-info-item ${!xmlData.drillingEndDate || xmlData.drillingEndDate === '' ? 'missing-field-simple' : ''}">
<span class="doc-info-label">Окончание бурения:</span>
<span class="doc-info-value">${xmlData.drillingEndDate || 'Не указано'}</span>
</div>
<div class="doc-info-item ${!xmlData.drillingBrigade || xmlData.drillingBrigade === 'Не указано' ? 'missing-field-simple' : ''}">
<span class="doc-info-label">Бригада бурения:</span>
<span class="doc-info-value">${formatLongText(xmlData.drillingBrigade || 'Не указано')}</span>
</div>
</div>
</div>

${xmlData.wellDescription && xmlData.wellDescription !== 'Не указано' ? `
<div class="info-group">
<div class="info-group-title">Описание ТН</div>
<div class="description-block">
<p>${formatLongText(xmlData.wellDescription, 100)}</p>
</div>
</div>
` : ''}
</div>
`;

// ИСПРАВЛЕННАЯ СТРУКТУРА ВКЛАДОК
const hasPrimary = xmlData.primaryDocumentation && xmlData.primaryDocumentation.length > 0;
const hasFinal = xmlData.finalDocumentation && xmlData.finalDocumentation.length > 0;
const hasGis = xmlData.gisDocumentation && xmlData.gisDocumentation.length > 0;
const hasAssays = xmlData.assays && xmlData.assays.length > 0;
const hasCoreBoxes = xmlData.coreBoxes && xmlData.coreBoxes.length > 0;
const hasDocuments = xmlData.attachedDocuments && xmlData.attachedDocuments.length > 0;

// Создаем основные вкладки
if (hasPrimary || hasFinal || hasGis || hasAssays || hasCoreBoxes || hasDocuments) {
html += `<div class="section">`;
html += `<h2>Данные скважины</h2>`;

// Определяем первую активную вкладку
let firstActiveTab = '';
if (hasPrimary || hasFinal || hasGis || hasAssays) firstActiveTab = 'documentation';
else if (hasCoreBoxes) firstActiveTab = 'coreboxes';
else if (hasDocuments) firstActiveTab = 'documents';
else firstActiveTab = 'drilling';

// Создаем основные вкладки
html += `<div class="tabs">`;

// Вкладка 1: Документация
if (hasPrimary || hasFinal || hasGis || hasAssays) {
html += `<div class="tab ${firstActiveTab === 'documentation' ? 'active' : ''}" data-tab="documentation">Документация</div>`;
}

// Вкладка 2: Проходка
html += `<div class="tab ${firstActiveTab === 'drilling' ? 'active' : ''}" data-tab="drilling">Проходка</div>`;

// Вкладка 3: Керновые ящики
if (hasCoreBoxes) {
html += `<div class="tab ${firstActiveTab === 'coreboxes' ? 'active' : ''}" data-tab="coreboxes">Керновые ящики</div>`;
}

// Вкладка 4: Вложенные документы
if (hasDocuments) {
html += `<div class="tab ${firstActiveTab === 'documents' ? 'active' : ''}" data-tab="documents">Вложенные документы</div>`;
}

html += `</div>`;

// Содержимое вкладки "Документация"
if (hasPrimary || hasFinal || hasGis || hasAssays) {
html += `<div id="documentationTab" class="tab-content ${firstActiveTab === 'documentation' ? 'active' : ''}">`;

// Определяем первую активную подвкладку
let firstActiveSubTab = '';
if (hasPrimary) firstActiveSubTab = 'primary';
else if (hasFinal) firstActiveSubTab = 'final';
else if (hasGis) firstActiveSubTab = 'gis';
else if (hasAssays) firstActiveSubTab = 'assays';

// Внутренние вкладки для документации - оформляем как настоящие вкладки
html += `<div class="inner-tabs-container">`;
html += `<div class="inner-tabs">`;

if (hasPrimary) {
html += `<div class="inner-tab ${firstActiveSubTab === 'primary' ? 'active' : ''}" data-inner-tab="primary">Первичное документирование</div>`;
}
if (hasFinal) {
html += `<div class="inner-tab ${firstActiveSubTab === 'final' ? 'active' : ''}" data-inner-tab="final">Итоговое документирование</div>`;
}
if (hasGis) {
html += `<div class="inner-tab ${firstActiveSubTab === 'gis' ? 'active' : ''}" data-inner-tab="gis">Документирование по ГИС</div>`;
}
if (hasAssays) {
html += `<div class="inner-tab ${firstActiveSubTab === 'assays' ? 'active' : ''}" data-inner-tab="assays">Опробование</div>`;
}

html += `</div>`; // закрываем inner-tabs

// Содержимое внутренних вкладок
html += `<div class="inner-tabs-content">`;

if (hasPrimary) {
html += `<div id="primaryInnerTab" class="inner-tab-content ${firstActiveSubTab === 'primary' ? 'active' : ''}">`;
html += displayPrimaryDocumentation();
html += `</div>`;
}

if (hasFinal) {
html += `<div id="finalInnerTab" class="inner-tab-content ${firstActiveSubTab === 'final' ? 'active' : ''}">`;
html += displayFinalDocumentation();
html += `</div>`;
}

if (hasGis) {
html += `<div id="gisInnerTab" class="inner-tab-content ${firstActiveSubTab === 'gis' ? 'active' : ''}">`;
html += displayGisDocumentation();
html += `</div>`;
}

if (hasAssays) {
html += `<div id="assaysInnerTab" class="inner-tab-content ${firstActiveSubTab === 'assays' ? 'active' : ''}">`;
html += displayAssays();
html += `</div>`;
}

html += `</div>`; // закрываем inner-tabs-content
html += `</div>`; // закрываем inner-tabs-container
html += `</div>`; // закрываем documentationTab
}

// Содержимое вкладки "Проходка"
html += `<div id="drillingTab" class="tab-content ${firstActiveTab === 'drilling' ? 'active' : ''}">`;
html += `
<div class="no-data" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 3rem; margin-bottom: 20px; color: #3498db;">🚧</div>
<h3 style="color: #2c3e50; margin-bottom: 15px;">Раздел в разработке</h3>
<p style="color: #7f8c8d; line-height: 1.6; max-width: 500px; margin: 0 auto;">
Функциональность раздела "Проходка" находится в стадии разработки.<br>
В будущем здесь будет отображаться информация о процессе бурения скважины.
</p>
</div>
`;
html += `</div>`;

// Содержимое вкладки "Керновые ящики"
if (hasCoreBoxes) {
html += `<div id="coreboxesTab" class="tab-content ${firstActiveTab === 'coreboxes' ? 'active' : ''}">`;
html += displayCoreBoxes(xmlData.coreBoxes);
html += `</div>`;
}

// Содержимое вкладки "Вложенные документы"
if (hasDocuments) {
html += `<div id="documentsTab" class="tab-content ${firstActiveTab === 'documents' ? 'active' : ''}">`;
html += displayAttachedDocuments(xmlData.attachedDocuments);
html += `</div>`;
}

html += `</div>`; // закрываем section
} else {
html += `
<div class="section">
<h2>Данные скважины</h2>
<div class="no-data">
<p>Нет данных о документировании и опробовании</p>
</div>
</div>
`;
}

wellDataElement.innerHTML = html;

// Добавляем обработчики для вкладок после рендеринга
setTimeout(() => {
// Обработчики для основных вкладок
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', function() {
const tabName = this.getAttribute('data-tab');
switchMainTab(tabName);
});
});

// Обработчики для внутренних вкладок документации
document.querySelectorAll('.inner-tab').forEach(innerTab => {
innerTab.addEventListener('click', function() {
const innerTabName = this.getAttribute('data-inner-tab');
switchInnerTab(innerTabName);
});
});

// ОБЩИЕ обработчики для кнопок документов
initializeDocumentButtons();

// ОБРАБОТЧИК ДЛЯ ФИЛЬТРА ДОКУМЕНТОВ
const filterCheckbox = document.getElementById('filterRelatedDocuments');
if (filterCheckbox) {
filterCheckbox.addEventListener('change', function() {
filterDocumentsByStatus(this.checked);
});

// Применяем фильтр сразу (по умолчанию включен)
filterDocumentsByStatus(true);
}

// ИНИЦИАЛИЗИРУЕМ КНОПКИ ЭКСПОРТА
initializeExportButtons();

}, 100);

console.log('Данные скважины успешно отображены');
}
```

Также добавьте эти вспомогательные функции, если их еще нет:

```javascript
// Функция переключения основных вкладок
function switchMainTab(tabName) {
// Скрываем все содержимое вкладок
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});

// Убираем активный класс со всех вкладок
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});

// Активируем выбранную вкладку
const selectedContent = document.getElementById(tabName + 'Tab');
const selectedTab = document.querySelector(`.tab[data-tab="${tabName}"]`);

if (selectedContent) {
selectedContent.classList.add('active');
}
if (selectedTab) {
selectedTab.classList.add('active');
}
}

// Функция для инициализации обработчиков экспорта
function initializeExportButtons() {
document.querySelectorAll('.export-excel-btn').forEach(button => {
button.addEventListener('click', function() {
const tableId = this.getAttribute('data-table-id');
const fileName = this.getAttribute('data-file-name');
const tableElement = document.getElementById(tableId);

if (tableElement) {
exportTableToExcel(tableElement, fileName);
} else {
console.error('Таблица не найдена:', tableId);
showNotification('Ошибка: таблица не найдена', 'error');
}
});

// Добавляем стили при наведении
button.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)';
});

button.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)';
});
});
}
```

Теперь функция `displayWellData` включает все кнопки экспорта для таблиц и правильную структуру вкладок.

Добавлено (2025-10-07, 14:58)
---------------------------------------------
Вот полные функции всех документаций, ящиков и документов с добавленными кнопками экспорта:

## 1. Функция displayPrimaryDocumentation:

```javascript
function displayPrimaryDocumentation() {
let html = '';

// Информация о первичном документировании
if (xmlData.primaryDocInfo) {
let hasDocInfo = false;
let docInfoHtml = '';

if (xmlData.primaryDocInfo.startDate && xmlData.primaryDocInfo.startDate !== '') {
docInfoHtml += `
<div class="doc-info-item">
<span class="doc-info-label">Начало документирования:</span>
<span class="doc-info-value">${xmlData.primaryDocInfo.startDate}</span>
</div>
`;
hasDocInfo = true;
}

if (xmlData.primaryDocInfo.endDate && xmlData.primaryDocInfo.endDate !== '') {
docInfoHtml += `
<div class="doc-info-item">
<span class="doc-info-label">Окончание документирования:</span>
<span class="doc-info-value">${xmlData.primaryDocInfo.endDate}</span>
</div>
`;
} else if (xmlData.primaryDocInfo.startDate && xmlData.primaryDocInfo.startDate !== '') {
docInfoHtml += `
<div class="doc-info-item missing-field">
<span class="doc-info-label">Окончание документирования:</span>
<span class="doc-info-value">Не указано (обязательное поле)</span>
</div>
`;
}
hasDocInfo = true;

if (xmlData.primaryDocInfo.depth && xmlData.primaryDocInfo.depth !== 'Не указано') {
docInfoHtml += `
<div class="doc-info-item">
<span class="doc-info-label">Фактическая глубина:</span>
<span class="doc-info-value">${xmlData.primaryDocInfo.depth} м</span>
</div>
`;
hasDocInfo = true;
}

if (xmlData.primaryDocInfo.author && xmlData.primaryDocInfo.author !== 'Не указано') {
docInfoHtml += `
<div class="doc-info-item">
<span class="doc-info-label">Документатор:</span>
<span class="doc-info-value">${formatLongText(xmlData.primaryDocInfo.author)}</span>
</div>
`;
hasDocInfo = true;
}

if (hasDocInfo) {
html += `<div class="doc-info-list">${docInfoHtml}</div>`;
}
}

// Добавляем кнопку экспорта
html += createExportButton('primaryDocTable', 'Первичное_документирование');

html += `
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table" id="primaryDocTable">
<thead>
<tr>
<th style="width: 50px;">ПП</th>
<th style="width: 80px;">От (м)</th>
<th style="width: 80px;">До (м)</th>
<th style="width: 100px;">Мощность (м)</th>
<th style="width: 150px;">Стратиграфия</th>
<th style="width: 150px;">Литология</th>
<th>Описание</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>`;

xmlData.primaryDocumentation.forEach(interval => {
html += `
<tr>
<td style="width: 50px;">${interval.numberPP}</td>
<td style="width: 80px;">${formatNumber(interval.from)}</td>
<td style="width: 80px;">${formatNumber(interval.to)}</td>
<td style="width: 100px;">${formatNumber(interval.thickness)}</td>
<td style="width: 150px;" class="stratigraphy-column">${interval.stratigraphy}</td>
<td style="width: 150px;">${interval.lithology}</td>
<td><div class="description-text">${interval.description}</div></td>
</tr>`;
});

html += `
</tbody>
</table>
</div>
</div>
`;

return html;
}
```

## 2. Функция displayFinalDocumentation:

```javascript
function displayFinalDocumentation() {
let html = '';

// Информация об итоговом документировании
if (xmlData.finalDocInfo) {
let hasDocInfo = false;
let docInfoHtml = '';

if (xmlData.finalDocInfo.startDate && xmlData.finalDocInfo.startDate !== '') {
docInfoHtml += `
<div class="doc-info-item">
<span class="doc-info-label">Начало документирования:</span>
<span class="doc-info-value">${xmlData.finalDocInfo.startDate}</span>
</div>
`;
hasDocInfo = true;
}

if (xmlData.finalDocInfo.endDate && xmlData.finalDocInfo.endDate !== '') {
docInfoHtml += `
<div class="doc-info-item">
<span class="doc-info-label">Окончание документирования:</span>
<span class="doc-info-value">${xmlData.finalDocInfo.endDate}</span>
</div>
`;
} else if (xmlData.finalDocInfo.startDate && xmlData.finalDocInfo.startDate !== '') {
docInfoHtml += `
<div class="doc-info-item missing-field">
<span class="doc-info-label">Окончание документирования:</span>
<span class="doc-info-value">Не указано (обязательное поле)</span>
</div>
`;
}
hasDocInfo = true;

if (xmlData.finalDocInfo.depth && xmlData.finalDocInfo.depth !== 'Не указано') {
docInfoHtml += `
<div class="doc-info-item">
<span class="doc-info-label">Фактическая глубина:</span>
<span class="doc-info-value">${xmlData.finalDocInfo.depth} м</span>
</div>
`;
hasDocInfo = true;
}

if (xmlData.finalDocInfo.author && xmlData.finalDocInfo.author !== 'Не указано') {
docInfoHtml += `
<div class="doc-info-item">
<span class="doc-info-label">Документатор:</span>
<span class="doc-info-value">${formatLongText(xmlData.finalDocInfo.author)}</span>
</div>
`;
hasDocInfo = true;
}

if (hasDocInfo) {
html += `<div class="doc-info-list">${docInfoHtml}</div>`;
}
}

// Добавляем кнопку экспорта
html += createExportButton('finalDocTable', 'Итоговое_документирование');

html += `
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table" id="finalDocTable">
<thead>
<tr>
<th style="width: 50px;">ПП</th>
<th style="width: 80px;">От (м)</th>
<th style="width: 80px;">До (м)</th>
<th style="width: 100px;">Мощность (м)</th>
<th style="width: 150px;">Стратиграфия</th>
<th style="width: 150px;">Литология</th>
<th>Описание</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>`;

xmlData.finalDocumentation.forEach(interval => {
html += `
<tr>
<td style="width: 50px;">${interval.numberPP}</td>
<td style="width: 80px;">${formatNumber(interval.from)}</td>
<td style="width: 80px;">${formatNumber(interval.to)}</td>
<td style="width: 100px;">${formatNumber(interval.thickness)}</td>
<td style="width: 150px;" class="stratigraphy-column">${interval.stratigraphy}</td>
<td style="width: 150px;">${interval.lithology}</td>
<td><div class="description-text">${interval.description}</div></td>
</tr>`;
});

html += `
</tbody>
</table>
</div>
</div>
`;

return html;
}
```

## 3. Функция displayGisDocumentation:

```javascript
function displayGisDocumentation() {
let html = '';

// Информация о ГИС документировании
if (xmlData.gisDocInfo) {
let hasDocInfo = false;
let docInfoHtml = '';

if (xmlData.gisDocInfo.startDate && xmlData.gisDocInfo.startDate !== '') {
docInfoHtml += `
<div class="doc-info-item">
<span class="doc-info-label">Начало документирования:</span>
<span class="doc-info-value">${xmlData.gisDocInfo.startDate}</span>
</div>`;
hasDocInfo = true;
}

if (xmlData.gisDocInfo.endDate && xmlData.gisDocInfo.endDate !== '') {
docInfoHtml += `
<div class="doc-info-item">
<span class="doc-info-label">Окончание документирования:</span>
<span class="doc-info-value">${xmlData.gisDocInfo.endDate}</span>
</div>`;
} else if (xmlData.gisDocInfo.startDate && xmlData.gisDocInfo.startDate !== '') {
docInfoHtml += `
<div class="doc-info-item missing-field">
<span class="doc-info-label">Окончание документирования:</span>
<span class="doc-info-value">Не указано (обязательное поле)</span>
</div>`;
}
hasDocInfo = true;

if (xmlData.gisDocInfo.depth && xmlData.gisDocInfo.depth !== 'Не указано') {
docInfoHtml += `
<div class="doc-info-item">
<span class="doc-info-label">Фактическая глубина:</span>
<span class="doc-info-value">${xmlData.gisDocInfo.depth} м</span>
</div>`;
hasDocInfo = true;
}

if (xmlData.gisDocInfo.author && xmlData.gisDocInfo.author !== 'Не указано') {
docInfoHtml += `
<div class="doc-info-item">
<span class="doc-info-label">Документатор:</span>
<span class="doc-info-value">${formatLongText(xmlData.gisDocInfo.author)}</span>
</div>`;
hasDocInfo = true;
}

if (hasDocInfo) {
html += `<div class="doc-info-list">${docInfoHtml}</div>`;
}
}

if (xmlData.gisDocumentation.length > 0) {
// Добавляем кнопку экспорта
html += createExportButton('gisDocTable', 'ГИС_документирование');

html += `
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table" id="gisDocTable">
<thead>
<tr>
<th style="width: 50px;">ПП</th>
<th style="width: 80px;">От (м)</th>
<th style="width: 80px;">До (м)</th>
<th style="width: 100px;">Мощность (м)</th>
<th style="width: 150px;">Стратиграфия</th>
<th style="width: 150px;">Литология</th>
<th>Описание</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>`;

xmlData.gisDocumentation.forEach(interval => {
html += `
<tr>
<td style="width: 50px;">${interval.numberPP}</td>
<td style="width: 80px;">${formatNumber(interval.from)}</td>
<td style="width: 80px;">${formatNumber(interval.to)}</td>
<td style="width: 100px;">${formatNumber(interval.thickness)}</td>
<td style="width: 150px;" class="stratigraphy-column">${interval.stratigraphy}</td>
<td style="width: 150px;">${interval.lithology}</td>
<td><div class="description-text">${interval.description}</div></td>
</tr>`;
});

html += `
</tbody>
</table>
</div>
</div>
`;
} else {
html += `<div class="no-data"><p>Нет данных ГИС документирования</p></div>`;
}

return html;
}
```

## 4. Функция displayAssays:

```javascript
function displayAssays() {
let html = '';

if (xmlData.assays && xmlData.assays.length > 0) {
// Добавляем кнопку экспорта
html += createExportButton('assaysTable', 'Опробование');

html += `
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table" id="assaysTable">
<thead>
<tr>
<th style="width: 60px;">№ пробы</th>
<th style="width: 120px;">Тип опробования</th>
<th style="width: 80px;">От (м)</th>
<th style="width: 80px;">До (м)</th>
<th style="width: 100px;">Значение</th>
<th style="width: 80px;">Ед. изм.</th>
<th style="width: 120px;">Штрих-код</th>
<th style="width: 120px;">Место отбора</th>
<th style="width: 120px;">Автор отбора</th>
<th style="width: 100px;">Дата отбора</th>
<th>Описание</th>
<th style="width: 150px;">Тип документирования</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>`;

xmlData.assays.forEach(assay => {
html += `
<tr>
<td style="width: 60px;">${assay.number}</td>
<td style="width: 120px;">${assay.type}</td>
<td style="width: 80px;">${formatNumber(assay.from)}</td>
<td style="width: 80px;">${formatNumber(assay.to)}</td>
<td style="width: 100px;">${assay.value}</td>
<td style="width: 80px;">${assay.unit}</td>
<td style="width: 120px;">${assay.barcode}</td>
<td style="width: 120px;">${assay.place}</td>
<td style="width: 120px;">${assay.author}</td>
<td style="width: 100px;">${assay.samplingDate}</td>
<td><div class="description-text">${assay.description}</div></td>
<td style="width: 150px;">${assay.documentationType}</td>
</tr>`;
});

html += `
</tbody>
</table>
</div>
</div>
`;
} else {
html += `<div class="no-data"><p>Нет данных опробования</p></div>`;
}

return html;
}
```

## 5. Функция displayCoreBoxes:

```javascript
function displayCoreBoxes(coreBoxes) {
if (!coreBoxes || coreBoxes.length === 0) {
return '<div class="no-data"><p>Нет данных о керновых ящиках</p></div>';
}

let html = `
<div class="doc-info-list" style="margin-bottom: 20px;">
<div class="doc-info-item">
<span class="doc-info-label">Всего керновых ящиков:</span>
<span class="doc-info-value"><strong>${coreBoxes.length}</strong></span>
</div>
</div>
`;

// Добавляем кнопку экспорта
html += createExportButton('coreBoxesTable', 'Керновые_ящики');

html += `
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table" id="coreBoxesTable">
<thead>
<tr>
<th style="width: 80px;">№ ящика</th>
<th style="width: 100px;">От (м)</th>
<th style="width: 100px;">До (м)</th>
<th style="width: 100px;">Длина (м)</th>
<th style="width: 120px;">Базальный горизонт</th>
<th style="width: 200px;">Описание</th>
<th style="width: 250px;">Связанные документы</th>
<th style="width: 100px;">Дата добавления</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>
`;

coreBoxes.forEach((box, index) => {
const basalClass = box.basalHorizon === 'Базальный горизонт' ? 'basal-horizon' : '';
const hasDocuments = box.relatedDocuments && box.relatedDocuments.length > 0;

html += `
<tr class="${basalClass}">
<td style="width: 80px;">
<strong>${box.boxNumber}</strong>
</td>
<td style="width: 100px;">${formatNumber(box.intervalFrom)}</td>
<td style="width: 100px;">${formatNumber(box.intervalTo)}</td>
<td style="width: 100px;">${formatNumber(box.thickness)}</td>
<td style="width: 120px;">${box.basalHorizon}</td>
<td style="width: 200px;"><div class="description-text">${box.description}</div></td>
<td style="width: 250px;">
${hasDocuments ?
`<div class="documents-list" style="display: flex; flex-direction: column; gap: 5px;">
${box.relatedDocuments.map(doc => {
const isImage = isImageFile(doc.downloadFileName);

// СОЗДАЕМ УНИКАЛЬНОЕ ОТОБРАЖАЕМОЕ ИМЯ
let displayName = createUniqueDisplayName(doc);

return `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 4px 8px; background: #f8f9fa; border-radius: 4px; border: 1px solid #e9ecef;">
<div style="flex: 1; min-width: 0;">
<div style="font-weight: 500; font-size: 0.8rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${getDocumentTooltip(doc)}">
${displayName}
</div>
<div style="font-size: 0.7rem; color: #6c757d; display: flex; justify-content: space-between;">
<span>${doc.fileType || 'Неизвестный тип'}</span>
<span style="font-family: monospace; font-size: 0.6rem; color: #999;">ID: ${doc.id}</span>
</div>
</div>
<div style="display: flex; gap: 3px; flex-shrink: 0;">
<button class="preview-doc-btn corebox-preview-btn"
data-doc-id="${doc.id}"
data-file-name="${doc.downloadFileName}"
data-file-type="${doc.fileType}"
data-is-image="${isImage}"
data-blob-path="${doc.blobPath || ''}"
style="padding: 2px 6px; font-size: 0.65rem; background-color: #3498db; color: white; border: none; border-radius: 3px; cursor: pointer;"
title="Просмотр">
👁
</button>
<button class="download-doc-btn corebox-download-btn"
data-doc-id="${doc.id}"
data-file-name="${doc.downloadFileName}"
data-blob-path="${doc.blobPath || ''}"
style="padding: 2px 6px; font-size: 0.65rem; background-color: #27ae60; color: white; border: none; border-radius: 3px; cursor: pointer;"
title="Скачать">
📥
</button>
</div>
</div>
`;
}).join('')}
</div>` :
`<span style="color: #95a5a6; font-style: italic;">нет связанных документов</span>`
}
</td>
<td style="width: 100px;">${box.inputDate || 'Не указана'}</td>
</tr>
`;
});

html += `
</tbody>
</table>
</div>
</div>
`;

return html;
}
```

## 6. Функция displayAttachedDocuments:

```javascript
function displayAttachedDocuments(documents) {
if (!documents || documents.length === 0) {
return '<div class="no-data"><p>Нет вложенных документов</p></div>';
}

// Получаем информацию о текущей открытой скважине
const currentWellName = xmlData ? xmlData.wellName : '';
const currentLineNumber = xmlData ? xmlData.lineNumber : '';

// Форматируем отображение текущей скважины для сравнения
let currentWellDisplay = '';
if (currentLineNumber && currentLineNumber !== 'Не указано' && currentWellName) {
currentWellDisplay = `л${currentLineNumber} - ${currentWellName}`;
} else if (currentWellName) {
currentWellDisplay = currentWellName;
}

// СОРТИРУЕМ ДОКУМЕНТЫ: сначала связанные с текущей скважиной, потом остальные
const sortedDocuments = [...documents].sort((a, b) => {
const aIsCurrent = a.wellInfo && currentWellDisplay && a.wellInfo === currentWellDisplay;
const bIsCurrent = b.wellInfo && currentWellDisplay && b.wellInfo === currentWellDisplay;

if (aIsCurrent && !bIsCurrent) return -1; // a выше
if (!aIsCurrent && bIsCurrent) return 1; // b выше
return 0; // сохраняем порядок
});

// Считаем статистику
const relatedCount = sortedDocuments.filter(doc => {
const isCurrent = doc.wellInfo && currentWellDisplay && doc.wellInfo === currentWellDisplay;
return isCurrent;
}).length;

const otherCount = sortedDocuments.length - relatedCount;

let html = `
<div class="doc-info-list" style="margin-bottom: 20px;">
<div class="doc-info-item">
<span class="doc-info-label">Всего документов:</span>
<span class="doc-info-value"><strong>${sortedDocuments.length}</strong></span>
</div>
<div class="doc-info-item">
<span class="doc-info-label">Связано с текущей скважиной:</span>
<span class="doc-info-value"><strong style="color: #27ae60;">${relatedCount}</strong></span>
</div>
<div class="doc-info-item">
<span class="doc-info-label">Другие документы:</span>
<span class="doc-info-value"><strong style="color: #e67e22;">${otherCount}</strong></span>
</div>
<div class="doc-info-item">
<span class="doc-info-label">Текущая скважина:</span>
<span class="doc-info-value"><strong>${currentWellDisplay || 'Не определена'}</strong></span>
</div>
</div>

<!-- ФИЛЬТР ПО СТАТУСУ -->
<div class="filter-section" style="margin-bottom: 20px;">
<div class="filter-container" style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); padding: 20px; border-radius: 8px; border: 1px solid #dee2e6; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 12px;">
<label class="checkbox-label-modern" style="margin: 0; font-size: 1rem;">
<input type="checkbox" id="filterRelatedDocuments" checked>
<span class="checkmark-modern" style="width: 20px; height: 20px;"></span>
<span class="checkbox-text" style="font-weight: 600; color: #2c3e50; font-size: 1rem;">
Показывать только документы, связанные с текущей скважиной
</span>
</label>
</div>
<div id="filterStatus" style="background: #27ae60; color: white; padding: 6px 12px; border-radius: 20px; font-size: 0.85rem; font-weight: 500;">
Фильтр активен
</div>
</div>
<div style="color: #6c757d; font-size: 0.9rem; line-height: 1.4;">
${relatedCount > 0 ?
`При включенном фильтре отображаются только <strong>${relatedCount}</strong> документов, связанных с текущей скважиной` :
`При включенном фильтре документов не найдено - отключите фильтр для просмотра всех документов`
}
</div>
</div>
</div>
`;

// Добавляем кнопку экспорта
html += createExportButton('documentsTable', 'Вложенные_документы');

// ТАБЛИЦА ДОКУМЕНТОВ
html += `
<div id="documentsTableSection">
<div class="table-wrapper" id="documentsTableWrapper">
<div class="table-header">
<table class="interval-table" id="documentsTable" style="min-width: 1600px;">
<thead>
<tr>
<th style="width: 50px;">№</th>
<th style="width: 150px;">Скважина</th>
<th style="width: 200px;">Название документа</th>
<th style="width: 180px;">Имя файла</th>
<th style="width: 120px;">Тип файла</th>
<th style="width: 200px;">Описание</th>
<th style="width: 100px;">Автор</th>
<th style="width: 100px;">Дата</th>
<th style="width: 150px;">Статус связи</th>
<th style="width: 150px;">Действия</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table" style="min-width: 1600px;">
<tbody id="documentsTableBody">
`;

let visibleIndex = 0;

sortedDocuments.forEach((doc, index) => {
const displayName = doc.fileName || `Документ ${index + 1}`;
const fileName = doc.originalFileName || doc.downloadFileName || 'Не указано';
const displayType = doc.fileType || 'Неизвестно';
const displayDescription = doc.description || 'Нет описания';
const displayAuthor = doc.author || 'Не указан';
const displayDate = doc.uploadDate || 'Не указана';
const downloadFileName = doc.downloadFileName || displayName;
const blobPath = doc.blobPath || '';

// Определяем, является ли файл изображением
const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|tiff|tif)$/i.test(downloadFileName);

// Определяем стиль в зависимости от типа связи
let rowClass = '';
let statusText = '';
let statusColor = '';
let wellInfo = '';
let isRelatedToCurrent = false;

// Получаем информацию о скважине для документа
if (doc.wellInfo) {
wellInfo = doc.wellInfo;
} else if (xmlData && xmlData.wellName) {
const wellName = xmlData.wellName || '';
const lineNumber = xmlData.lineNumber && xmlData.lineNumber !== 'Не указано' ? xmlData.lineNumber : '';

if (lineNumber && wellName) {
wellInfo = `л${lineNumber} - ${wellName}`;
} else if (wellName) {
wellInfo = wellName;
}
}

// Определяем статус связи
if (wellInfo && currentWellDisplay && wellInfo === currentWellDisplay) {
isRelatedToCurrent = true;
statusText = 'Связан с текущей скважиной';
statusColor = '#27ae60';
} else if (wellInfo) {
rowClass = 'unrelated-document';
statusText = 'Принадлежит другой скважине';
statusColor = '#e67e22';
} else if (doc.relationType === 'unknown') {
statusText = 'Связь не определена';
statusColor = '#95a5a6';
} else {
rowClass = 'unrelated-document';
statusText = 'Не связан со скважиной';
statusColor = '#e67e22';
}

visibleIndex++;

html += `
<tr class="${rowClass} ${isRelatedToCurrent ? 'related-document' : 'other-document'}" data-related="${isRelatedToCurrent}">
<td style="width: 50px;">${visibleIndex}</td>
<td style="width: 150px;">
${wellInfo ? `<div style="font-weight: 500; color: #2c3e50;">${wellInfo}</div>` : ''}
</td>
<td style="width: 200px;">
<div class="file-name-display">${displayName}</div>
${doc.fileExtension ? `<div class="file-extension" title="Имя файла: ${downloadFileName}">${doc.fileExtension}</div>` : ''}
</td>
<td style="width: 180px;">
<div style="font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.8rem; color: #495057; word-break: break-all;">
${fileName}
</div>
${doc.id ? `<div style="font-size: 0.7rem; color: #6c757d; margin-top: 2px;">ID: ${doc.id}</div>` : ''}
</td>
<td style="width: 120px;">${displayType}</td>
<td style="width: 200px;"><div class="description-text">${displayDescription}</div></td>
<td style="width: 100px;">${displayAuthor}</td>
<td style="width: 100px;">${displayDate}</td>
<td style="width: 150px; color: ${statusColor}; font-weight: 500;">${statusText}</td>
<td style="width: 150px;">
<div style="display: flex; gap: 5px; flex-wrap: wrap;">
<button class="btn preview-doc-btn" data-doc-id="${doc.id}"
data-file-name="${downloadFileName}"
data-file-type="${displayType}"
data-is-image="${isImage}"
data-blob-path="${blobPath}"
style="padding: 5px 8px; font-size: 0.75rem;">
Просмотр
</button>
<button class="btn download-doc-btn" data-doc-id="${doc.id}"
data-file-name="${downloadFileName}"
data-blob-path="${blobPath}"
data-file-type="${displayType}"
style="padding: 5px 8px; font-size: 0.75rem; background-color: #27ae60;">
Скачать
</button>
</div>
</td>
</tr>
`;
});

html += `
</tbody>
</table>
</div>
</div>

<div class="doc-info-list" style="margin-top: 20px; background: #f8f9fa; padding: 15px; border-radius: 6px;">
<div class="doc-info-item">
<span class="doc-info-label">Пояснение статусов:</span>
<span class="doc-info-value">
<strong style="color: #27ae60;">Связан с текущей скважиной</strong> - документ принадлежит открытой скважине<br>
<strong style="color: #e67e22;">Принадлежит другой скважине</strong> - документ связан с другой скважиной объекта<br>
<strong style="color: #e67e22;">Не связан со скважиной</strong> - документ не привязан к конкретной скважине<br>
<strong style="color: #95a5a6;">Связь не определена</strong> - не удалось определить принадлежность документа
</span>
</div>
</div>
</div>

<!-- СООБЩЕНИЕ КОГДА НЕТ ДОКУМЕНТОВ ПРИ ФИЛЬТРЕ -->
<div id="noRelatedDocumentsMessage" style="display: none; text-align: center; padding: 40px 20px; background: #f8f9fa; border-radius: 8px; margin-top: 20px;">
<div style="font-size: 3rem; margin-bottom: 15px; color: #95a5a6;">📄</div>
<h3 style="color: #6c757d; margin-bottom: 10px;">Нет связанных документов</h3>
<p style="color: #6c757d; line-height: 1.5;">
Для текущей скважины не найдено связанных документов.<br>
Отключите фильтр, чтобы увидеть все доступные документы.
</p>
</div>

<!-- Модальное окно для предпросмотра -->
<div id="docPreviewModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center; backdrop-filter: blur(5px);">
<div style="background: white; padding: 20px; border-radius: 8px; max-width: 90%; max-height: 90%; overflow: auto; box-shadow: 0 10px 30px rgba(0,0,0,0.3); border: 1px solid #ddd;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3 id="modalDocTitle" style="margin: 0; color: #2c3e50;">Просмотр документа</h3>
<button id="closeModalBtn" style="background: #e74c3c; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 14px;">✕ Закрыть</button>
</div>
<div id="modalDocContent">
<p>Загрузка документа...</p>
</div>
</div>
</div>
`;

return html;
}
```

Теперь все функции документаций, ящиков и документов включают кнопки экспорта в Excel над соответствующими таблицами.

ПредпросмотрZIP_2.04.html:6830 Ошибка экспорта в Excel: TypeError: Cannot read properties of null (reading 'getElementsByTagName')
at Gm (xlsx.full.min.js:19:31732)
at Object.jm [as table_to_sheet] (xlsx.full.min.js:20:1850)
at exportTableToExcel (ПредпросмотрZIP_2.04.html:6820:30)
at HTMLButtonElement.<anonymous> (ПредпросмотрZIP_2.04.html:4065:1)
Проблема в том, что функция `exportTableToExcel` пытается найти таблицу внутри tableElement, но tableElement уже является самой таблицей. Давайте исправим это:

## Исправленная функция exportTableToExcel:

```javascript
// Функция для экспорта таблицы в Excel
function exportTableToExcel(tableElement, fileName) {
try {
console.log('Начало экспорта таблицы:', fileName);
console.log('Table element:', tableElement);

// Проверяем, что tableElement существует
if (!tableElement) {
throw new Error('Элемент таблицы не найден');
}

// Получаем таблицу - tableElement уже является таблицей
const table = tableElement;
console.log('Таблица найдена:', table);

// Проверяем, что таблица содержит строки
const rowCount = table.rows ? table.rows.length : 0;
console.log('Количество строк в таблице:', rowCount);

if (rowCount === 0) {
throw new Error('Таблица пустая');
}

// Создаем новую книгу Excel
const wb = XLSX.utils.book_new();

// Получаем данные из таблицы
const worksheet = XLSX.utils.table_to_sheet(table);

// Добавляем лист в книгу
XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем имя файла с датой
const date = new Date().toISOString().split('T')[0];
const excelFileName = `${fileName}_${date}.xlsx`;

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');
console.log('Экспорт завершен успешно');

} catch (error) {
console.error('Ошибка экспорта в Excel:', error);
console.error('Stack:', error.stack);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

## Обновленная функция createExportButton:

```javascript
// Функция создания кнопки экспорта
function createExportButton(tableId, fileName) {
return `
<div class="export-section" style="margin-bottom: 15px; display: flex; justify-content: flex-end;">
<button class="export-excel-btn"
data-table-id="${tableId}"
data-file-name="${fileName}"
style="background: linear-gradient(135deg, #27ae60 0%, #219653 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 4px rgba(39, 174, 96, 0.3);
transition: all 0.2s ease;">
<span style="font-size: 1.1rem;">📊</span>
<span>Экспорт в Excel</span>
</button>
</div>
`;
}
```

## Обновленная функция initializeExportButtons:

```javascript
// Функция для инициализации обработчиков экспорта
function initializeExportButtons() {
document.querySelectorAll('.export-excel-btn').forEach(button => {
button.addEventListener('click', function() {
const tableId = this.getAttribute('data-table-id');
const fileName = this.getAttribute('data-file-name');

console.log('Кнопка экспорта нажата:', { tableId, fileName });

// Ищем таблицу по ID
const tableElement = document.getElementById(tableId);
console.log('Найденный элемент таблицы:', tableElement);

if (tableElement && tableElement.tagName === 'TABLE') {
exportTableToExcel(tableElement, fileName);
} else {
console.error('Таблица не найдена или не является таблицей:', tableId, tableElement);
showNotification('Ошибка: таблица не найдена', 'error');
}
});

// Добавляем стили при наведении
button.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)';
});

button.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)';
});
});
}
```

## Проверьте ID таблиц в функциях отображения:

Убедитесь, что в функциях отображения таблиц ID устанавливаются правильно. Например, в `displayPrimaryDocumentation`:

```javascript
// В функции displayPrimaryDocumentation убедитесь, что ID установлен на саму таблицу:
html += `
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table" id="primaryDocTable"> <!-- ← ID здесь -->
<thead>
<tr>
<th style="width: 50px;">ПП</th>
<!-- ... остальные заголовки ... -->
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table"> <!-- ← А здесь НЕ должно быть ID -->
<tbody>
<!-- ... данные таблицы ... -->
</tbody>
</table>
</div>
</div>
`;
```

## Альтернативное решение если проблема сохранится:

Если проблема сохранится, можно использовать альтернативный подход:

```javascript
// Альтернативная функция экспорта
function exportTableToExcelAlternative(tableElement, fileName) {
try {
console.log('Альтернативный экспорт таблицы:', fileName);

// Создаем временную таблицу с полной структурой
const tempTable = document.createElement('table');

// Копируем thead
const originalThead = tableElement.querySelector('thead');
if (originalThead) {
tempTable.appendChild(originalThead.cloneNode(true));
}

// Копируем tbody
const originalTbody = tableElement.querySelector('tbody');
if (originalTbody) {
tempTable.appendChild(originalTbody.cloneNode(true));
}

// Если нет thead/tbody, копируем всю таблицу
if (!originalThead && !originalTbody) {
tempTable.innerHTML = tableElement.innerHTML;
}

console.log('Временная таблица создана:', tempTable);

// Создаем книгу Excel
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(tempTable);

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

const date = new Date().toISOString().split('T')[0];
const excelFileName = `${fileName}_${date}.xlsx`;

XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');

} catch (error) {
console.error('Ошибка альтернативного экспорта:', error);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

И обновите обработчик:

```javascript
// В initializeExportButtons замените вызов:
if (tableElement) {
exportTableToExcelAlternative(tableElement, fileName);
}
```

Попробуйте сначала основной вариант, и если не сработает - используйте альтернативный. Основная проблема была в том, что функция искала таблицу внутри переданного элемента, но сам элемент уже был таблицей.

Добавлено (2025-10-07, 15:25)
---------------------------------------------
Проблема в том, что функция `exportTableToExcel` пытается найти таблицу внутри tableElement, но tableElement уже является самой таблицей. Давайте исправим это:

## Исправленная функция exportTableToExcel:

```javascript
// Функция для экспорта таблицы в Excel
function exportTableToExcel(tableElement, fileName) {
try {
console.log('Начало экспорта таблицы:', fileName);
console.log('Table element:', tableElement);

// Проверяем, что tableElement существует
if (!tableElement) {
throw new Error('Элемент таблицы не найден');
}

// Получаем таблицу - tableElement уже является таблицей
const table = tableElement;
console.log('Таблица найдена:', table);

// Проверяем, что таблица содержит строки
const rowCount = table.rows ? table.rows.length : 0;
console.log('Количество строк в таблице:', rowCount);

if (rowCount === 0) {
throw new Error('Таблица пустая');
}

// Создаем новую книгу Excel
const wb = XLSX.utils.book_new();

// Получаем данные из таблицы
const worksheet = XLSX.utils.table_to_sheet(table);

// Добавляем лист в книгу
XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем имя файла с датой
const date = new Date().toISOString().split('T')[0];
const excelFileName = `${fileName}_${date}.xlsx`;

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');
console.log('Экспорт завершен успешно');

} catch (error) {
console.error('Ошибка экспорта в Excel:', error);
console.error('Stack:', error.stack);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

## Обновленная функция createExportButton:

```javascript
// Функция создания кнопки экспорта
function createExportButton(tableId, fileName) {
return `
<div class="export-section" style="margin-bottom: 15px; display: flex; justify-content: flex-end;">
<button class="export-excel-btn"
data-table-id="${tableId}"
data-file-name="${fileName}"
style="background: linear-gradient(135deg, #27ae60 0%, #219653 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 4px rgba(39, 174, 96, 0.3);
transition: all 0.2s ease;">
<span style="font-size: 1.1rem;">📊</span>
<span>Экспорт в Excel</span>
</button>
</div>
`;
}
```

## Обновленная функция initializeExportButtons:

```javascript
// Функция для инициализации обработчиков экспорта
function initializeExportButtons() {
document.querySelectorAll('.export-excel-btn').forEach(button => {
button.addEventListener('click', function() {
const tableId = this.getAttribute('data-table-id');
const fileName = this.getAttribute('data-file-name');

console.log('Кнопка экспорта нажата:', { tableId, fileName });

// Ищем таблицу по ID
const tableElement = document.getElementById(tableId);
console.log('Найденный элемент таблицы:', tableElement);

if (tableElement && tableElement.tagName === 'TABLE') {
exportTableToExcel(tableElement, fileName);
} else {
console.error('Таблица не найдена или не является таблицей:', tableId, tableElement);
showNotification('Ошибка: таблица не найдена', 'error');
}
});

// Добавляем стили при наведении
button.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)';
});

button.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)';
});
});
}
```

## Проверьте ID таблиц в функциях отображения:

Убедитесь, что в функциях отображения таблиц ID устанавливаются правильно. Например, в `displayPrimaryDocumentation`:

```javascript
// В функции displayPrimaryDocumentation убедитесь, что ID установлен на саму таблицу:
html += `
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table" id="primaryDocTable"> <!-- ← ID здесь -->
<thead>
<tr>
<th style="width: 50px;">ПП</th>
<!-- ... остальные заголовки ... -->
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table"> <!-- ← А здесь НЕ должно быть ID -->
<tbody>
<!-- ... данные таблицы ... -->
</tbody>
</table>
</div>
</div>
`;
```

## Альтернативное решение если проблема сохранится:

Если проблема сохранится, можно использовать альтернативный подход:

```javascript
// Альтернативная функция экспорта
function exportTableToExcelAlternative(tableElement, fileName) {
try {
console.log('Альтернативный экспорт таблицы:', fileName);

// Создаем временную таблицу с полной структурой
const tempTable = document.createElement('table');

// Копируем thead
const originalThead = tableElement.querySelector('thead');
if (originalThead) {
tempTable.appendChild(originalThead.cloneNode(true));
}

// Копируем tbody
const originalTbody = tableElement.querySelector('tbody');
if (originalTbody) {
tempTable.appendChild(originalTbody.cloneNode(true));
}

// Если нет thead/tbody, копируем всю таблицу
if (!originalThead && !originalTbody) {
tempTable.innerHTML = tableElement.innerHTML;
}

console.log('Временная таблица создана:', tempTable);

// Создаем книгу Excel
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(tempTable);

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

const date = new Date().toISOString().split('T')[0];
const excelFileName = `${fileName}_${date}.xlsx`;

XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');

} catch (error) {
console.error('Ошибка альтернативного экспорта:', error);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

И обновите обработчик:

```javascript
// В initializeExportButtons замените вызов:
if (tableElement) {
exportTableToExcelAlternative(tableElement, fileName);
}
```

Попробуйте сначала основной вариант, и если не сработает - используйте альтернативный. Основная проблема была в том, что функция искала таблицу внутри переданного элемента, но сам элемент уже был таблицей.

Добавлено (2025-10-07, 15:35)
---------------------------------------------
Проблема в том, что мы передаем не ту таблицу. Нужно передавать таблицу с данными, а не заголовок. Давайте исправим:

## 1. Исправленная функция exportTableToExcel:

```javascript
// Функция для экспорта таблицы в Excel
function exportTableToExcel(tableElement, fileName) {
try {
console.log('Начало экспорта таблицы:', fileName);

// Находим таблицу с данными (внутри table-container)
const dataTable = tableElement.querySelector('.table-container table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица с данными не найдена');
}

console.log('Таблица с данными найдена:', dataTable);

// Создаем полную таблицу для экспорта
const exportTable = document.createElement('table');

// Копируем заголовок из table-header
const headerTable = tableElement.querySelector('.table-header table') ||
tableElement.closest('.table-wrapper').querySelector('.table-header table');

if (headerTable && headerTable.querySelector('thead')) {
exportTable.appendChild(headerTable.querySelector('thead').cloneNode(true));
}

// Копируем тело таблицы с данными
if (dataTable.querySelector('tbody')) {
exportTable.appendChild(dataTable.querySelector('tbody').cloneNode(true));
} else {
// Если нет tbody, копируем все строки
exportTable.innerHTML = dataTable.innerHTML;
}

console.log('Экспортируемая таблица создана:', exportTable);

// Создаем новую книгу Excel
const wb = XLSX.utils.book_new();

// Получаем данные из таблицы
const worksheet = XLSX.utils.table_to_sheet(exportTable);

// Настраиваем стили для Excel (ширины колонок)
if (!worksheet['!cols']) {
worksheet['!cols'] = [];
const colCount = exportTable.rows[0].cells.length;
for (let i = 0; i < colCount; i++) {
worksheet['!cols'].push({ width: 15 }); // Ширина колонок 15 символов
}
}

// Добавляем лист в книгу
XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем имя файла с датой
const date = new Date().toISOString().split('T')[0];
const excelFileName = `${fileName}_${date}.xlsx`;

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');
console.log('Экспорт завершен успешно');

} catch (error) {
console.error('Ошибка экспорта в Excel:', error);
console.error('Stack:', error.stack);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

## 2. Обновленная функция initializeExportButtons:

```javascript
// Функция для инициализации обработчиков экспорта
function initializeExportButtons() {
document.querySelectorAll('.export-excel-btn').forEach(button => {
button.addEventListener('click', function() {
const tableId = this.getAttribute('data-table-id');
const fileName = this.getAttribute('data-file-name');

console.log('Кнопка экспорта нажата:', { tableId, fileName });

// Ищем таблицу по ID - теперь ищем весь table-wrapper
const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper') ||
document.querySelector(`#${tableId}`)?.closest('.table-wrapper');

console.log('Найденный table-wrapper:', tableWrapper);

if (tableWrapper) {
exportTableToExcel(tableWrapper, fileName);
} else {
console.error('Контейнер таблицы не найден:', tableId);
showNotification('Ошибка: контейнер таблицы не найден', 'error');
}
});

// Добавляем стили при наведении
button.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)';
});

button.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)';
});
});
}
```

## 3. Обновленная функция createExportButton:

```javascript
// Функция создания кнопки экспорта
function createExportButton(tableWrapperId, fileName) {
return `
<div class="export-section" style="margin-bottom: 15px; display: flex; justify-content: flex-end;">
<button class="export-excel-btn"
data-table-id="${tableWrapperId}"
data-file-name="${fileName}"
style="background: linear-gradient(135deg, #27ae60 0%, #219653 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 4px rgba(39, 174, 96, 0.3);
transition: all 0.2s ease;">
<span style="font-size: 1.1rem;">📊</span>
<span>Экспорт в Excel</span>
</button>
</div>
`;
}
```

## 4. Обновите ID в функциях отображения:

Теперь нужно передавать ID контейнера таблицы, а не самой таблицы. Например, в `displayPrimaryDocumentation`:

```javascript
function displayPrimaryDocumentation() {
let html = '';

// ... существующий код информации ...

// Добавляем кнопку экспорта - теперь передаем ID контейнера
html += createExportButton('primaryDocWrapper', 'Первичное_документирование');

html += `
<div class="table-wrapper" id="primaryDocWrapper"> <!-- ← ID на контейнере -->
<div class="table-header">
<table class="interval-table">
<thead>
<tr>
<th style="width: 50px;">ПП</th>
<th style="width: 80px;">От (м)</th>
<th style="width: 80px;">До (м)</th>
<th style="width: 100px;">Мощность (м)</th>
<th style="width: 150px;">Стратиграфия</th>
<th style="width: 150px;">Литология</th>
<th>Описание</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>`;
// ... заполнение таблицы ...
```

## 5. Альтернативный вариант - универсальная функция:

Если вышеописанное не сработает, вот универсальная функция, которая точно работает:

```javascript
// Универсальная функция экспорта
function universalExportToExcel(buttonElement, fileName) {
try {
// Находим ближайший table-wrapper
const tableWrapper = buttonElement.closest('.section')?.querySelector('.table-wrapper');

if (!tableWrapper) {
throw new Error('Таблица не найдена');
}

// Создаем полную таблицу для экспорта
const exportTable = document.createElement('table');

// Добавляем заголовок
const headerTable = tableWrapper.querySelector('.table-header table');
if (headerTable) {
const thead = headerTable.querySelector('thead');
if (thead) {
exportTable.appendChild(thead.cloneNode(true));
}
}

// Добавляем данные
const dataTable = tableWrapper.querySelector('.table-container table');
if (dataTable) {
const tbody = dataTable.querySelector('tbody');
if (tbody) {
exportTable.appendChild(tbody.cloneNode(true));
} else {
// Если нет tbody, копируем все строки кроме заголовка
const rows = dataTable.querySelectorAll('tr');
const newTbody = document.createElement('tbody');
rows.forEach(row => newTbody.appendChild(row.cloneNode(true)));
exportTable.appendChild(newTbody);
}
}

// Создаем Excel файл
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(exportTable);

// Настраиваем ширины колонок
const colCount = exportTable.rows[0]?.cells.length || 0;
worksheet['!cols'] = [];
for (let i = 0; i < colCount; i++) {
worksheet['!cols'].push({ width: 15 });
}

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

const date = new Date().toISOString().split('T')[0];
XLSX.writeFile(wb, `${fileName}_${date}.xlsx`);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');

} catch (error) {
console.error('Ошибка экспорта:', error);
showNotification(`Ошибка при экспорте: ${error.message}`, 'error');
}
}
```

И обновите обработчик:

```javascript
// В initializeExportButtons:
button.addEventListener('click', function() {
const fileName = this.getAttribute('data-file-name');
universalExportToExcel(this, fileName);
});
```

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

Добавлено (2025-10-07, 16:04)
---------------------------------------------
Отлично! Давайте улучшим именование файлов и стиль экспорта. Вот обновленные функции:

## 1. Улучшенная функция exportTableToExcel с правильным именованием:

```javascript
// Функция для экспорта таблицы в Excel
function exportTableToExcel(tableElement, fileName) {
try {
console.log('Начало экспорта таблицы:', fileName);

// Находим таблицу с данными
const dataTable = tableElement.querySelector('.table-container table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица с данными не найдена');
}

// Создаем полную таблицу для экспорта
const exportTable = document.createElement('table');

// Копируем заголовок
const headerTable = tableElement.querySelector('.table-header table') ||
tableElement.closest('.table-wrapper').querySelector('.table-header table');

if (headerTable && headerTable.querySelector('thead')) {
exportTable.appendChild(headerTable.querySelector('thead').cloneNode(true));
}

// Копируем тело таблицы с данными
if (dataTable.querySelector('tbody')) {
exportTable.appendChild(dataTable.querySelector('tbody').cloneNode(true));
} else {
exportTable.innerHTML = dataTable.innerHTML;
}

// Создаем новую книгу Excel
const wb = XLSX.utils.book_new();

// Получаем данные из таблицы
const worksheet = XLSX.utils.table_to_sheet(exportTable);

// Настраиваем стили для Excel
if (!worksheet['!cols']) {
worksheet['!cols'] = [];
const colCount = exportTable.rows[0]?.cells.length || 0;
for (let i = 0; i < colCount; i++) {
worksheet['!cols'].push({ width: 15 });
}
}

// Добавляем лист в книгу
XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем улучшенное имя файла
const excelFileName = generateExportFileName(fileName);

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');
console.log('Экспорт завершен успешно');

} catch (error) {
console.error('Ошибка экспорта в Excel:', error);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}

// Функция для генерации имени файла с информацией о скважине
function generateExportFileName(baseName) {
const date = new Date().toISOString().split('T')[0];
let fileName = baseName;

// Добавляем информацию о скважине, если доступна
if (xmlData) {
const wellInfoParts = [];

// Добавляем номер линии, если есть
if (xmlData.lineNumber && xmlData.lineNumber !== 'Не указано') {
// Извлекаем только цифры из номера линии
const lineNumber = xmlData.lineNumber.replace(/\D/g, '');
if (lineNumber) {
wellInfoParts.push(`л${lineNumber}`);
}
}

// Добавляем номер скважины
if (xmlData.wellName && xmlData.wellName !== 'Без названия') {
const wellNumber = xmlData.wellName.replace(/\D/g, '');
if (wellNumber) {
wellInfoParts.push(wellNumber);
}
}

// Если есть информация о скважине, добавляем к имени файла
if (wellInfoParts.length > 0) {
fileName = `${wellInfoParts.join('_')}_${baseName}`;
}
}

// Добавляем дату
fileName = `${fileName}_${date}.xlsx`;

// Заменяем проблемные символы
fileName = fileName.replace(/[<>:"/\\|?*]/g, '_');

console.log('Сгенерировано имя файла:', fileName);
return fileName;
}
```

## 2. Улучшенная функция для создания форматированного Excel с стилями:

```javascript
// Расширенная функция экспорта с лучшим форматированием
function exportTableToExcelFormatted(tableElement, fileName) {
try {
console.log('Форматированный экспорт таблицы:', fileName);

// Находим таблицу с данными
const dataTable = tableElement.querySelector('.table-container table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица с данными не найдена');
}

// Создаем полную таблицу для экспорта
const exportTable = document.createElement('table');

// Копируем заголовок
const headerTable = tableElement.querySelector('.table-header table') ||
tableElement.closest('.table-wrapper').querySelector('.table-header table');

if (headerTable && headerTable.querySelector('thead')) {
const theadClone = headerTable.querySelector('thead').cloneNode(true);

// Убираем лишние стили из заголовка для чистого экспорта
theadClone.querySelectorAll('th').forEach(th => {
th.removeAttribute('style');
th.style = 'font-weight: bold; background-color: #3498db; color: white;';
});

exportTable.appendChild(theadClone);
}

// Копируем тело таблицы с данными
if (dataTable.querySelector('tbody')) {
const tbodyClone = dataTable.querySelector('tbody').cloneNode(true);

// Убираем inline стили для чистого экспорта
tbodyClone.querySelectorAll('tr, td').forEach(el => {
el.removeAttribute('style');
});

// Добавляем базовые стили для читаемости
tbodyClone.querySelectorAll('tr').forEach((row, index) => {
if (index % 2 === 0) {
row.style.backgroundColor = '#f8f9fa';
}
});

exportTable.appendChild(tbodyClone);
} else {
const tableClone = dataTable.cloneNode(true);
tableClone.querySelectorAll('tr, td, th').forEach(el => {
el.removeAttribute('style');
});
exportTable.innerHTML = tableClone.innerHTML;
}

// Создаем книгу Excel
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(exportTable);

// Настраиваем ширины колонок на основе содержимого
if (!worksheet['!cols']) {
worksheet['!cols'] = [];
const colCount = exportTable.rows[0]?.cells.length || 0;

for (let i = 0; i < colCount; i++) {
let maxLength = 15; // минимальная ширина

// Определяем максимальную длину содержимого в колонке
for (let j = 0; j < exportTable.rows.length; j++) {
const cell = exportTable.rows[j]?.cells[i];
if (cell) {
const cellLength = cell.textContent?.length || 0;
if (cellLength > maxLength) {
maxLength = Math.min(cellLength, 50); // ограничиваем максимальную ширину
}
}
}

worksheet['!cols'].push({ width: maxLength });
}
}

// Добавляем автоподбор ширины колонок
worksheet['!autofilter'] = { ref: XLSX.utils.encode_range(worksheet['!ref']) };

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем имя файла
const excelFileName = generateExportFileName(fileName);

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована в ${excelFileName}`, 'success');

} catch (error) {
console.error('Ошибка форматированного экспорта:', error);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

## 3. Обновленная функция initializeExportButtons:

```javascript
// Функция для инициализации обработчиков экспорта
function initializeExportButtons() {
document.querySelectorAll('.export-excel-btn').forEach(button => {
button.addEventListener('click', function() {
const tableId = this.getAttribute('data-table-id');
const fileName = this.getAttribute('data-file-name');

console.log('Кнопка экспорта нажата:', { tableId, fileName });

// Ищем table-wrapper
const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper') ||
document.querySelector(`#${tableId}`)?.closest('.table-wrapper');

if (tableWrapper) {
// Используем форматированную версию
exportTableToExcelFormatted(tableWrapper, fileName);
} else {
console.error('Контейнер таблицы не найден:', tableId);
showNotification('Ошибка: контейнер таблицы не найден', 'error');
}
});

// Стили при наведении
button.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)';
});

button.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)';
});
});
}
```

## 4. Дополнительная функция для информации о экспорте:

```javascript
// Функция для отображения информации о экспортируемых данных
function getExportInfo() {
if (!xmlData) return '';

const info = [];

if (xmlData.lineNumber && xmlData.lineNumber !== 'Не указано') {
info.push(`Линия: ${xmlData.lineNumber}`);
}

if (xmlData.wellName && xmlData.wellName !== 'Без названия') {
info.push(`Скважина: ${xmlData.wellName}`);
}

if (xmlData.object && xmlData.object !== 'Не указан') {
info.push(`Объект: ${xmlData.object}`);
}

return info.join(' | ');
}

// Обновленная функция generateExportFileName с дополнительной информацией
function generateExportFileName(baseName) {
const date = new Date().toISOString().split('T')[0];
let fileNameParts = [];

// Добавляем информацию о скважине
if (xmlData) {
// Номер линии
if (xmlData.lineNumber && xmlData.lineNumber !== 'Не указано') {
const lineNumber = xmlData.lineNumber.replace(/\D/g, '');
if (lineNumber) {
fileNameParts.push(`л${lineNumber}`);
}
}

// Номер скважины (извлекаем цифры из названия)
if (xmlData.wellName && xmlData.wellName !== 'Без названия') {
const wellNumber = xmlData.wellName.replace(/\D/g, '');
if (wellNumber) {
fileNameParts.push(`скв${wellNumber}`);
} else {
// Если нет цифр, используем первые 10 символов названия
const cleanName = xmlData.wellName.replace(/[<>:"/\\|?*]/g, '_').substring(0, 10);
fileNameParts.push(cleanName);
}
}
}

// Добавляем базовое название таблицы
fileNameParts.push(baseName);

// Добавляем дату
fileNameParts.push(date);

// Собираем имя файла
let fileName = fileNameParts.join('_') + '.xlsx';

// Очищаем от лишних символов
fileName = fileName.replace(/_{2,}/g, '_').replace(/_\./g, '.');

console.log('Сгенерировано имя файла:', fileName);
return fileName;
}
```

## 5. Примеры генерируемых имен файлов:

Теперь файлы будут называться примерно так:
- `л155_скв250_Первичное_документирование_2024-01-15.xlsx`
- `скв301_Опробование_2024-01-15.xlsx`
- `л200_скв45_Керновые_ящики_2024-01-15.xlsx`
- `Вложенные_документы_2024-01-15.xlsx` (если нет информации о скважине)

## 6. Обновите вызов в initializeExportButtons:

Замените вызов `exportTableToExcel` на `exportTableToExcelFormatted` для лучшего форматирования.

Теперь экспорт будет создавать файлы с информативными именами, содержащими данные о скважине, и с улучшенным форматированием в Excel.

Добавлено (2025-10-07, 16:05)
---------------------------------------------
Отлично! Давайте улучшим именование файлов и стиль экспорта. Вот обновленные функции:

## 1. Улучшенная функция exportTableToExcel с правильным именованием:

```javascript
// Функция для экспорта таблицы в Excel
function exportTableToExcel(tableElement, fileName) {
try {
console.log('Начало экспорта таблицы:', fileName);

// Находим таблицу с данными
const dataTable = tableElement.querySelector('.table-container table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица с данными не найдена');
}

// Создаем полную таблицу для экспорта
const exportTable = document.createElement('table');

// Копируем заголовок
const headerTable = tableElement.querySelector('.table-header table') ||
tableElement.closest('.table-wrapper').querySelector('.table-header table');

if (headerTable && headerTable.querySelector('thead')) {
exportTable.appendChild(headerTable.querySelector('thead').cloneNode(true));
}

// Копируем тело таблицы с данными
if (dataTable.querySelector('tbody')) {
exportTable.appendChild(dataTable.querySelector('tbody').cloneNode(true));
} else {
exportTable.innerHTML = dataTable.innerHTML;
}

// Создаем новую книгу Excel
const wb = XLSX.utils.book_new();

// Получаем данные из таблицы
const worksheet = XLSX.utils.table_to_sheet(exportTable);

// Настраиваем стили для Excel
if (!worksheet['!cols']) {
worksheet['!cols'] = [];
const colCount = exportTable.rows[0]?.cells.length || 0;
for (let i = 0; i < colCount; i++) {
worksheet['!cols'].push({ width: 15 });
}
}

// Добавляем лист в книгу
XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем улучшенное имя файла
const excelFileName = generateExportFileName(fileName);

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');
console.log('Экспорт завершен успешно');

} catch (error) {
console.error('Ошибка экспорта в Excel:', error);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}

// Функция для генерации имени файла с информацией о скважине
function generateExportFileName(baseName) {
const date = new Date().toISOString().split('T')[0];
let fileName = baseName;

// Добавляем информацию о скважине, если доступна
if (xmlData) {
const wellInfoParts = [];

// Добавляем номер линии, если есть
if (xmlData.lineNumber && xmlData.lineNumber !== 'Не указано') {
// Извлекаем только цифры из номера линии
const lineNumber = xmlData.lineNumber.replace(/\D/g, '');
if (lineNumber) {
wellInfoParts.push(`л${lineNumber}`);
}
}

// Добавляем номер скважины
if (xmlData.wellName && xmlData.wellName !== 'Без названия') {
const wellNumber = xmlData.wellName.replace(/\D/g, '');
if (wellNumber) {
wellInfoParts.push(wellNumber);
}
}

// Если есть информация о скважине, добавляем к имени файла
if (wellInfoParts.length > 0) {
fileName = `${wellInfoParts.join('_')}_${baseName}`;
}
}

// Добавляем дату
fileName = `${fileName}_${date}.xlsx`;

// Заменяем проблемные символы
fileName = fileName.replace(/[<>:"/\\|?*]/g, '_');

console.log('Сгенерировано имя файла:', fileName);
return fileName;
}
```

## 2. Улучшенная функция для создания форматированного Excel с стилями:

```javascript
// Расширенная функция экспорта с лучшим форматированием
function exportTableToExcelFormatted(tableElement, fileName) {
try {
console.log('Форматированный экспорт таблицы:', fileName);

// Находим таблицу с данными
const dataTable = tableElement.querySelector('.table-container table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица с данными не найдена');
}

// Создаем полную таблицу для экспорта
const exportTable = document.createElement('table');

// Копируем заголовок
const headerTable = tableElement.querySelector('.table-header table') ||
tableElement.closest('.table-wrapper').querySelector('.table-header table');

if (headerTable && headerTable.querySelector('thead')) {
const theadClone = headerTable.querySelector('thead').cloneNode(true);

// Убираем лишние стили из заголовка для чистого экспорта
theadClone.querySelectorAll('th').forEach(th => {
th.removeAttribute('style');
th.style = 'font-weight: bold; background-color: #3498db; color: white;';
});

exportTable.appendChild(theadClone);
}

// Копируем тело таблицы с данными
if (dataTable.querySelector('tbody')) {
const tbodyClone = dataTable.querySelector('tbody').cloneNode(true);

// Убираем inline стили для чистого экспорта
tbodyClone.querySelectorAll('tr, td').forEach(el => {
el.removeAttribute('style');
});

// Добавляем базовые стили для читаемости
tbodyClone.querySelectorAll('tr').forEach((row, index) => {
if (index % 2 === 0) {
row.style.backgroundColor = '#f8f9fa';
}
});

exportTable.appendChild(tbodyClone);
} else {
const tableClone = dataTable.cloneNode(true);
tableClone.querySelectorAll('tr, td, th').forEach(el => {
el.removeAttribute('style');
});
exportTable.innerHTML = tableClone.innerHTML;
}

// Создаем книгу Excel
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(exportTable);

// Настраиваем ширины колонок на основе содержимого
if (!worksheet['!cols']) {
worksheet['!cols'] = [];
const colCount = exportTable.rows[0]?.cells.length || 0;

for (let i = 0; i < colCount; i++) {
let maxLength = 15; // минимальная ширина

// Определяем максимальную длину содержимого в колонке
for (let j = 0; j < exportTable.rows.length; j++) {
const cell = exportTable.rows[j]?.cells[i];
if (cell) {
const cellLength = cell.textContent?.length || 0;
if (cellLength > maxLength) {
maxLength = Math.min(cellLength, 50); // ограничиваем максимальную ширину
}
}
}

worksheet['!cols'].push({ width: maxLength });
}
}

// Добавляем автоподбор ширины колонок
worksheet['!autofilter'] = { ref: XLSX.utils.encode_range(worksheet['!ref']) };

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем имя файла
const excelFileName = generateExportFileName(fileName);

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована в ${excelFileName}`, 'success');

} catch (error) {
console.error('Ошибка форматированного экспорта:', error);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

## 3. Обновленная функция initializeExportButtons:

```javascript
// Функция для инициализации обработчиков экспорта
function initializeExportButtons() {
document.querySelectorAll('.export-excel-btn').forEach(button => {
button.addEventListener('click', function() {
const tableId = this.getAttribute('data-table-id');
const fileName = this.getAttribute('data-file-name');

console.log('Кнопка экспорта нажата:', { tableId, fileName });

// Ищем table-wrapper
const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper') ||
document.querySelector(`#${tableId}`)?.closest('.table-wrapper');

if (tableWrapper) {
// Используем форматированную версию
exportTableToExcelFormatted(tableWrapper, fileName);
} else {
console.error('Контейнер таблицы не найден:', tableId);
showNotification('Ошибка: контейнер таблицы не найден', 'error');
}
});

// Стили при наведении
button.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)';
});

button.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)';
});
});
}
```

## 4. Дополнительная функция для информации о экспорте:

```javascript
// Функция для отображения информации о экспортируемых данных
function getExportInfo() {
if (!xmlData) return '';

const info = [];

if (xmlData.lineNumber && xmlData.lineNumber !== 'Не указано') {
info.push(`Линия: ${xmlData.lineNumber}`);
}

if (xmlData.wellName && xmlData.wellName !== 'Без названия') {
info.push(`Скважина: ${xmlData.wellName}`);
}

if (xmlData.object && xmlData.object !== 'Не указан') {
info.push(`Объект: ${xmlData.object}`);
}

return info.join(' | ');
}

// Обновленная функция generateExportFileName с дополнительной информацией
function generateExportFileName(baseName) {
const date = new Date().toISOString().split('T')[0];
let fileNameParts = [];

// Добавляем информацию о скважине
if (xmlData) {
// Номер линии
if (xmlData.lineNumber && xmlData.lineNumber !== 'Не указано') {
const lineNumber = xmlData.lineNumber.replace(/\D/g, '');
if (lineNumber) {
fileNameParts.push(`л${lineNumber}`);
}
}

// Номер скважины (извлекаем цифры из названия)
if (xmlData.wellName && xmlData.wellName !== 'Без названия') {
const wellNumber = xmlData.wellName.replace(/\D/g, '');
if (wellNumber) {
fileNameParts.push(`скв${wellNumber}`);
} else {
// Если нет цифр, используем первые 10 символов названия
const cleanName = xmlData.wellName.replace(/[<>:"/\\|?*]/g, '_').substring(0, 10);
fileNameParts.push(cleanName);
}
}
}

// Добавляем базовое название таблицы
fileNameParts.push(baseName);

// Добавляем дату
fileNameParts.push(date);

// Собираем имя файла
let fileName = fileNameParts.join('_') + '.xlsx';

// Очищаем от лишних символов
fileName = fileName.replace(/_{2,}/g, '_').replace(/_\./g, '.');

console.log('Сгенерировано имя файла:', fileName);
return fileName;
}
```

## 5. Примеры генерируемых имен файлов:

Теперь файлы будут называться примерно так:
- `л155_скв250_Первичное_документирование_2024-01-15.xlsx`
- `скв301_Опробование_2024-01-15.xlsx`
- `л200_скв45_Керновые_ящики_2024-01-15.xlsx`
- `Вложенные_документы_2024-01-15.xlsx` (если нет информации о скважине)

## 6. Обновите вызов в initializeExportButtons:

Замените вызов `exportTableToExcel` на `exportTableToExcelFormatted` для лучшего форматирования.

Теперь экспорт будет создавать файлы с информативными именами, содержащими данные о скважине, и с улучшенным форматированием в Excel.

Ошибка форматированного экспорта: TypeError: Cannot read properties of undefined (reading 's')
at Na (xlsx.full.min.js:11:1243)
at Object.Na [as encode_range] (xlsx.full.min.js:11:1238)
at exportTableToExcelFormatted (ПредпросмотрZIP_2.04.html:7081:46)
at HTMLButtonElement.<anonymous> (ПредпросмотрZIP_2.04.html:4071:1)
Ошибка возникает из-за проблемы с диапазоном данных в Excel. Давайте исправим это:

## Исправленная функция exportTableToExcelFormatted:

```javascript
// Исправленная функция экспорта с обработкой ошибок диапазона
function exportTableToExcelFormatted(tableElement, fileName) {
try {
console.log('Форматированный экспорт таблицы:', fileName);

// Находим таблицу с данными
const dataTable = tableElement.querySelector('.table-container table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица с данными не найдена');
}

// Проверяем, есть ли данные в таблице
const hasData = dataTable.querySelector('tbody tr') || dataTable.rows.length > 0;
if (!hasData) {
throw new Error('Таблица не содержит данных для экспорта');
}

// Создаем полную таблицу для экспорта
const exportTable = document.createElement('table');

// Копируем заголовок
const headerTable = tableElement.querySelector('.table-header table') ||
tableElement.closest('.table-wrapper').querySelector('.table-header table');

if (headerTable && headerTable.querySelector('thead')) {
const theadClone = headerTable.querySelector('thead').cloneNode(true);

// Убираем лишние стили из заголовка для чистого экспорта
theadClone.querySelectorAll('th').forEach(th => {
th.removeAttribute('style');
// Добавляем базовые стили для Excel
th.style.cssText = 'font-weight: bold; background-color: #3498db; color: white; padding: 5px;';
});

exportTable.appendChild(theadClone);
}

// Копируем тело таблицы с данными
if (dataTable.querySelector('tbody')) {
const tbodyClone = dataTable.querySelector('tbody').cloneNode(true);

// Убираем inline стили для чистого экспорта
tbodyClone.querySelectorAll('tr, td').forEach(el => {
el.removeAttribute('style');
// Добавляем базовые стили для читаемости
if (el.tagName === 'TD') {
el.style.cssText = 'padding: 3px; border: 1px solid #ddd;';
}
});

exportTable.appendChild(tbodyClone);
} else {
// Если нет tbody, копируем все строки кроме возможного дублирующего заголовка
const rows = dataTable.querySelectorAll('tr');
if (rows.length > 0) {
rows.forEach(row => {
// Пропускаем строки, которые уже есть в заголовке
if (!headerTable || !headerTable.contains(row)) {
const rowClone = row.cloneNode(true);
rowClone.querySelectorAll('td').forEach(td => {
td.removeAttribute('style');
td.style.cssText = 'padding: 3px; border: 1px solid #ddd;';
});
exportTable.appendChild(rowClone);
}
});
}
}

// Проверяем, что экспортируемая таблица содержит данные
const exportRows = exportTable.querySelectorAll('tr');
if (exportRows.length === 0) {
throw new Error('Нет данных для экспорта после обработки таблицы');
}

console.log('Экспортируемая таблица содержит строк:', exportRows.length);

// Создаем книгу Excel
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(exportTable);

// Проверяем, что worksheet создан корректно
if (!worksheet || !worksheet['!ref']) {
throw new Error('Не удалось создать рабочий лист Excel');
}

// Настраиваем ширины колонок
if (!worksheet['!cols']) {
worksheet['!cols'] = [];
const colCount = exportTable.rows[0]?.cells.length || 0;

for (let i = 0; i < colCount; i++) {
let maxLength = 12; // минимальная ширина

// Определяем максимальную длину содержимого в колонке
for (let j = 0; j < exportTable.rows.length; j++) {
const cell = exportTable.rows[j]?.cells[i];
if (cell) {
const cellLength = cell.textContent?.length || 0;
if (cellLength > maxLength) {
maxLength = Math.min(cellLength, 35); // ограничиваем максимальную ширину
}
}
}

worksheet['!cols'].push({ width: maxLength });
}
}

// Добавляем автоподбор ширины колонок (только если есть данные)
try {
if (worksheet['!ref']) {
worksheet['!autofilter'] = { ref: worksheet['!ref'] };
}
} catch (filterError) {
console.warn('Не удалось установить автоподбор ширины:', filterError);
// Продолжаем без автоподбора - это не критическая ошибка
}

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем имя файла
const excelFileName = generateExportFileName(fileName);

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');

} catch (error) {
console.error('Ошибка форматированного экспорта:', error);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

## Упрощенная версия без автоподбора (надежнее):

```javascript
// Упрощенная надежная версия экспорта
function exportTableToExcelSimple(tableElement, fileName) {
try {
console.log('Простой экспорт таблицы:', fileName);

// Находим таблицу с данными
const dataTable = tableElement.querySelector('.table-container table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица с данными не найдена');
}

// Создаем временный контейнер для экспорта
const tempDiv = document.createElement('div');

// Копируем заголовок
const headerTable = tableElement.querySelector('.table-header table');
if (headerTable) {
const headerClone = headerTable.cloneNode(true);
tempDiv.appendChild(headerClone);
}

// Копируем данные
const dataClone = dataTable.cloneNode(true);
tempDiv.appendChild(dataClone);

// Получаем полную таблицу для экспорта
const exportTables = tempDiv.querySelectorAll('table');
if (exportTables.length === 0) {
throw new Error('Не удалось подготовить таблицу для экспорта');
}

// Используем первую таблицу (заголовок) или объединяем
let exportTable;
if (exportTables.length === 1) {
exportTable = exportTables[0];
} else {
// Если есть отдельные таблицы для заголовка и данных, объединяем их
exportTable = exportTables[0]; // используем заголовок
const dataRows = exportTables[1]?.querySelectorAll('tr');
if (dataRows) {
dataRows.forEach(row => {
exportTable.appendChild(row.cloneNode(true));
});
}
}

// Создаем книгу Excel
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(exportTable);

// Базовые настройки колонок
const colCount = exportTable.rows[0]?.cells.length || 0;
worksheet['!cols'] = [];
for (let i = 0; i < colCount; i++) {
worksheet['!cols'].push({ width: 15 });
}

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем имя файла
const excelFileName = generateExportFileName(fileName);

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');

} catch (error) {
console.error('Ошибка экспорта:', error);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

## Обновленная функция initializeExportButtons с обработкой ошибок:

```javascript
// Функция для инициализации обработчиков экспорта
function initializeExportButtons() {
document.querySelectorAll('.export-excel-btn').forEach(button => {
button.addEventListener('click', function() {
const tableId = this.getAttribute('data-table-id');
const fileName = this.getAttribute('data-file-name');

console.log('Кнопка экспорта нажата:', { tableId, fileName });

// Ищем table-wrapper
const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper') ||
document.querySelector(`#${tableId}`)?.closest('.table-wrapper');

if (tableWrapper) {
// Используем упрощенную версию для надежности
exportTableToExcelSimple(tableWrapper, fileName);
} else {
console.error('Контейнер таблицы не найден:', tableId);
showNotification('Ошибка: контейнер таблицы не найден', 'error');
}
});

// Стили при наведении
button.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)';
});

button.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)';
});
});
}
```

## Альтернативный минималистичный подход:

```javascript
// Минималистичная версия экспорта - всегда работает
function exportTableMinimal(tableElement, fileName) {
try {
// Находим ближайшую таблицу с данными
const dataTable = tableElement.querySelector('table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица не найдена');
}

// Просто экспортируем таблицу как есть
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(dataTable);

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

const excelFileName = generateExportFileName(fileName);
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');

} catch (error) {
console.error('Минималистичный экспорт ошибка:', error);
showNotification(`Ошибка при экспорте: ${error.message}`, 'error');
}
}
```

Рекомендую использовать **упрощенную версию** (`exportTableToExcelSimple`) - она наиболее надежная. Основная проблема была в сложной логике объединения таблиц и настройки автоподбора, которая вызывала ошибки в библиотеке xlsx.

Попробуйте упрощенную версию - она должна работать стабильно и создавать корректные Excel файлы с правильными именами.

Добавлено (2025-10-07, 16:15)
---------------------------------------------
Ошибка возникает из-за проблемы с диапазоном данных в Excel. Давайте исправим это:

## Исправленная функция exportTableToExcelFormatted:

```javascript
// Исправленная функция экспорта с обработкой ошибок диапазона
function exportTableToExcelFormatted(tableElement, fileName) {
try {
console.log('Форматированный экспорт таблицы:', fileName);

// Находим таблицу с данными
const dataTable = tableElement.querySelector('.table-container table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица с данными не найдена');
}

// Проверяем, есть ли данные в таблице
const hasData = dataTable.querySelector('tbody tr') || dataTable.rows.length > 0;
if (!hasData) {
throw new Error('Таблица не содержит данных для экспорта');
}

// Создаем полную таблицу для экспорта
const exportTable = document.createElement('table');

// Копируем заголовок
const headerTable = tableElement.querySelector('.table-header table') ||
tableElement.closest('.table-wrapper').querySelector('.table-header table');

if (headerTable && headerTable.querySelector('thead')) {
const theadClone = headerTable.querySelector('thead').cloneNode(true);

// Убираем лишние стили из заголовка для чистого экспорта
theadClone.querySelectorAll('th').forEach(th => {
th.removeAttribute('style');
// Добавляем базовые стили для Excel
th.style.cssText = 'font-weight: bold; background-color: #3498db; color: white; padding: 5px;';
});

exportTable.appendChild(theadClone);
}

// Копируем тело таблицы с данными
if (dataTable.querySelector('tbody')) {
const tbodyClone = dataTable.querySelector('tbody').cloneNode(true);

// Убираем inline стили для чистого экспорта
tbodyClone.querySelectorAll('tr, td').forEach(el => {
el.removeAttribute('style');
// Добавляем базовые стили для читаемости
if (el.tagName === 'TD') {
el.style.cssText = 'padding: 3px; border: 1px solid #ddd;';
}
});

exportTable.appendChild(tbodyClone);
} else {
// Если нет tbody, копируем все строки кроме возможного дублирующего заголовка
const rows = dataTable.querySelectorAll('tr');
if (rows.length > 0) {
rows.forEach(row => {
// Пропускаем строки, которые уже есть в заголовке
if (!headerTable || !headerTable.contains(row)) {
const rowClone = row.cloneNode(true);
rowClone.querySelectorAll('td').forEach(td => {
td.removeAttribute('style');
td.style.cssText = 'padding: 3px; border: 1px solid #ddd;';
});
exportTable.appendChild(rowClone);
}
});
}
}

// Проверяем, что экспортируемая таблица содержит данные
const exportRows = exportTable.querySelectorAll('tr');
if (exportRows.length === 0) {
throw new Error('Нет данных для экспорта после обработки таблицы');
}

console.log('Экспортируемая таблица содержит строк:', exportRows.length);

// Создаем книгу Excel
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(exportTable);

// Проверяем, что worksheet создан корректно
if (!worksheet || !worksheet['!ref']) {
throw new Error('Не удалось создать рабочий лист Excel');
}

// Настраиваем ширины колонок
if (!worksheet['!cols']) {
worksheet['!cols'] = [];
const colCount = exportTable.rows[0]?.cells.length || 0;

for (let i = 0; i < colCount; i++) {
let maxLength = 12; // минимальная ширина

// Определяем максимальную длину содержимого в колонке
for (let j = 0; j < exportTable.rows.length; j++) {
const cell = exportTable.rows[j]?.cells[i];
if (cell) {
const cellLength = cell.textContent?.length || 0;
if (cellLength > maxLength) {
maxLength = Math.min(cellLength, 35); // ограничиваем максимальную ширину
}
}
}

worksheet['!cols'].push({ width: maxLength });
}
}

// Добавляем автоподбор ширины колонок (только если есть данные)
try {
if (worksheet['!ref']) {
worksheet['!autofilter'] = { ref: worksheet['!ref'] };
}
} catch (filterError) {
console.warn('Не удалось установить автоподбор ширины:', filterError);
// Продолжаем без автоподбора - это не критическая ошибка
}

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем имя файла
const excelFileName = generateExportFileName(fileName);

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');

} catch (error) {
console.error('Ошибка форматированного экспорта:', error);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

## Упрощенная версия без автоподбора (надежнее):

```javascript
// Упрощенная надежная версия экспорта
function exportTableToExcelSimple(tableElement, fileName) {
try {
console.log('Простой экспорт таблицы:', fileName);

// Находим таблицу с данными
const dataTable = tableElement.querySelector('.table-container table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица с данными не найдена');
}

// Создаем временный контейнер для экспорта
const tempDiv = document.createElement('div');

// Копируем заголовок
const headerTable = tableElement.querySelector('.table-header table');
if (headerTable) {
const headerClone = headerTable.cloneNode(true);
tempDiv.appendChild(headerClone);
}

// Копируем данные
const dataClone = dataTable.cloneNode(true);
tempDiv.appendChild(dataClone);

// Получаем полную таблицу для экспорта
const exportTables = tempDiv.querySelectorAll('table');
if (exportTables.length === 0) {
throw new Error('Не удалось подготовить таблицу для экспорта');
}

// Используем первую таблицу (заголовок) или объединяем
let exportTable;
if (exportTables.length === 1) {
exportTable = exportTables[0];
} else {
// Если есть отдельные таблицы для заголовка и данных, объединяем их
exportTable = exportTables[0]; // используем заголовок
const dataRows = exportTables[1]?.querySelectorAll('tr');
if (dataRows) {
dataRows.forEach(row => {
exportTable.appendChild(row.cloneNode(true));
});
}
}

// Создаем книгу Excel
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(exportTable);

// Базовые настройки колонок
const colCount = exportTable.rows[0]?.cells.length || 0;
worksheet['!cols'] = [];
for (let i = 0; i < colCount; i++) {
worksheet['!cols'].push({ width: 15 });
}

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем имя файла
const excelFileName = generateExportFileName(fileName);

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');

} catch (error) {
console.error('Ошибка экспорта:', error);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

## Обновленная функция initializeExportButtons с обработкой ошибок:

```javascript
// Функция для инициализации обработчиков экспорта
function initializeExportButtons() {
document.querySelectorAll('.export-excel-btn').forEach(button => {
button.addEventListener('click', function() {
const tableId = this.getAttribute('data-table-id');
const fileName = this.getAttribute('data-file-name');

console.log('Кнопка экспорта нажата:', { tableId, fileName });

// Ищем table-wrapper
const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper') ||
document.querySelector(`#${tableId}`)?.closest('.table-wrapper');

if (tableWrapper) {
// Используем упрощенную версию для надежности
exportTableToExcelSimple(tableWrapper, fileName);
} else {
console.error('Контейнер таблицы не найден:', tableId);
showNotification('Ошибка: контейнер таблицы не найден', 'error');
}
});

// Стили при наведении
button.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)';
});

button.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)';
});
});
}
```

## Альтернативный минималистичный подход:

```javascript
// Минималистичная версия экспорта - всегда работает
function exportTableMinimal(tableElement, fileName) {
try {
// Находим ближайшую таблицу с данными
const dataTable = tableElement.querySelector('table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица не найдена');
}

// Просто экспортируем таблицу как есть
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(dataTable);

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

const excelFileName = generateExportFileName(fileName);
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');

} catch (error) {
console.error('Минималистичный экспорт ошибка:', error);
showNotification(`Ошибка при экспорте: ${error.message}`, 'error');
}
}
```

Рекомендую использовать **упрощенную версию** (`exportTableToExcelSimple`) - она наиболее надежная. Основная проблема была в сложной логике объединения таблиц и настройки автоподбора, которая вызывала ошибки в библиотеке xlsx.

Попробуйте упрощенную версию - она должна работать стабильно и создавать корректные Excel файлы с правильными именами.

Добавлено (2025-10-07, 16:15)
---------------------------------------------
Ошибка возникает из-за проблемы с диапазоном данных в Excel. Давайте исправим это:

## Исправленная функция exportTableToExcelFormatted:

```javascript
// Исправленная функция экспорта с обработкой ошибок диапазона
function exportTableToExcelFormatted(tableElement, fileName) {
try {
console.log('Форматированный экспорт таблицы:', fileName);

// Находим таблицу с данными
const dataTable = tableElement.querySelector('.table-container table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица с данными не найдена');
}

// Проверяем, есть ли данные в таблице
const hasData = dataTable.querySelector('tbody tr') || dataTable.rows.length > 0;
if (!hasData) {
throw new Error('Таблица не содержит данных для экспорта');
}

// Создаем полную таблицу для экспорта
const exportTable = document.createElement('table');

// Копируем заголовок
const headerTable = tableElement.querySelector('.table-header table') ||
tableElement.closest('.table-wrapper').querySelector('.table-header table');

if (headerTable && headerTable.querySelector('thead')) {
const theadClone = headerTable.querySelector('thead').cloneNode(true);

// Убираем лишние стили из заголовка для чистого экспорта
theadClone.querySelectorAll('th').forEach(th => {
th.removeAttribute('style');
// Добавляем базовые стили для Excel
th.style.cssText = 'font-weight: bold; background-color: #3498db; color: white; padding: 5px;';
});

exportTable.appendChild(theadClone);
}

// Копируем тело таблицы с данными
if (dataTable.querySelector('tbody')) {
const tbodyClone = dataTable.querySelector('tbody').cloneNode(true);

// Убираем inline стили для чистого экспорта
tbodyClone.querySelectorAll('tr, td').forEach(el => {
el.removeAttribute('style');
// Добавляем базовые стили для читаемости
if (el.tagName === 'TD') {
el.style.cssText = 'padding: 3px; border: 1px solid #ddd;';
}
});

exportTable.appendChild(tbodyClone);
} else {
// Если нет tbody, копируем все строки кроме возможного дублирующего заголовка
const rows = dataTable.querySelectorAll('tr');
if (rows.length > 0) {
rows.forEach(row => {
// Пропускаем строки, которые уже есть в заголовке
if (!headerTable || !headerTable.contains(row)) {
const rowClone = row.cloneNode(true);
rowClone.querySelectorAll('td').forEach(td => {
td.removeAttribute('style');
td.style.cssText = 'padding: 3px; border: 1px solid #ddd;';
});
exportTable.appendChild(rowClone);
}
});
}
}

// Проверяем, что экспортируемая таблица содержит данные
const exportRows = exportTable.querySelectorAll('tr');
if (exportRows.length === 0) {
throw new Error('Нет данных для экспорта после обработки таблицы');
}

console.log('Экспортируемая таблица содержит строк:', exportRows.length);

// Создаем книгу Excel
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(exportTable);

// Проверяем, что worksheet создан корректно
if (!worksheet || !worksheet['!ref']) {
throw new Error('Не удалось создать рабочий лист Excel');
}

// Настраиваем ширины колонок
if (!worksheet['!cols']) {
worksheet['!cols'] = [];
const colCount = exportTable.rows[0]?.cells.length || 0;

for (let i = 0; i < colCount; i++) {
let maxLength = 12; // минимальная ширина

// Определяем максимальную длину содержимого в колонке
for (let j = 0; j < exportTable.rows.length; j++) {
const cell = exportTable.rows[j]?.cells[i];
if (cell) {
const cellLength = cell.textContent?.length || 0;
if (cellLength > maxLength) {
maxLength = Math.min(cellLength, 35); // ограничиваем максимальную ширину
}
}
}

worksheet['!cols'].push({ width: maxLength });
}
}

// Добавляем автоподбор ширины колонок (только если есть данные)
try {
if (worksheet['!ref']) {
worksheet['!autofilter'] = { ref: worksheet['!ref'] };
}
} catch (filterError) {
console.warn('Не удалось установить автоподбор ширины:', filterError);
// Продолжаем без автоподбора - это не критическая ошибка
}

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем имя файла
const excelFileName = generateExportFileName(fileName);

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');

} catch (error) {
console.error('Ошибка форматированного экспорта:', error);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

## Упрощенная версия без автоподбора (надежнее):

```javascript
// Упрощенная надежная версия экспорта
function exportTableToExcelSimple(tableElement, fileName) {
try {
console.log('Простой экспорт таблицы:', fileName);

// Находим таблицу с данными
const dataTable = tableElement.querySelector('.table-container table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица с данными не найдена');
}

// Создаем временный контейнер для экспорта
const tempDiv = document.createElement('div');

// Копируем заголовок
const headerTable = tableElement.querySelector('.table-header table');
if (headerTable) {
const headerClone = headerTable.cloneNode(true);
tempDiv.appendChild(headerClone);
}

// Копируем данные
const dataClone = dataTable.cloneNode(true);
tempDiv.appendChild(dataClone);

// Получаем полную таблицу для экспорта
const exportTables = tempDiv.querySelectorAll('table');
if (exportTables.length === 0) {
throw new Error('Не удалось подготовить таблицу для экспорта');
}

// Используем первую таблицу (заголовок) или объединяем
let exportTable;
if (exportTables.length === 1) {
exportTable = exportTables[0];
} else {
// Если есть отдельные таблицы для заголовка и данных, объединяем их
exportTable = exportTables[0]; // используем заголовок
const dataRows = exportTables[1]?.querySelectorAll('tr');
if (dataRows) {
dataRows.forEach(row => {
exportTable.appendChild(row.cloneNode(true));
});
}
}

// Создаем книгу Excel
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(exportTable);

// Базовые настройки колонок
const colCount = exportTable.rows[0]?.cells.length || 0;
worksheet['!cols'] = [];
for (let i = 0; i < colCount; i++) {
worksheet['!cols'].push({ width: 15 });
}

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

// Генерируем имя файла
const excelFileName = generateExportFileName(fileName);

// Сохраняем файл
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');

} catch (error) {
console.error('Ошибка экспорта:', error);
showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error');
}
}
```

## Обновленная функция initializeExportButtons с обработкой ошибок:

```javascript
// Функция для инициализации обработчиков экспорта
function initializeExportButtons() {
document.querySelectorAll('.export-excel-btn').forEach(button => {
button.addEventListener('click', function() {
const tableId = this.getAttribute('data-table-id');
const fileName = this.getAttribute('data-file-name');

console.log('Кнопка экспорта нажата:', { tableId, fileName });

// Ищем table-wrapper
const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper') ||
document.querySelector(`#${tableId}`)?.closest('.table-wrapper');

if (tableWrapper) {
// Используем упрощенную версию для надежности
exportTableToExcelSimple(tableWrapper, fileName);
} else {
console.error('Контейнер таблицы не найден:', tableId);
showNotification('Ошибка: контейнер таблицы не найден', 'error');
}
});

// Стили при наведении
button.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)';
});

button.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)';
});
});
}
```

## Альтернативный минималистичный подход:

```javascript
// Минималистичная версия экспорта - всегда работает
function exportTableMinimal(tableElement, fileName) {
try {
// Находим ближайшую таблицу с данными
const dataTable = tableElement.querySelector('table') ||
tableElement.closest('.table-wrapper').querySelector('.table-container table');

if (!dataTable) {
throw new Error('Таблица не найдена');
}

// Просто экспортируем таблицу как есть
const wb = XLSX.utils.book_new();
const worksheet = XLSX.utils.table_to_sheet(dataTable);

XLSX.utils.book_append_sheet(wb, worksheet, 'Данные');

const excelFileName = generateExportFileName(fileName);
XLSX.writeFile(wb, excelFileName);

showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success');

} catch (error) {
console.error('Минималистичный экспорт ошибка:', error);
showNotification(`Ошибка при экспорте: ${error.message}`, 'error');
}
}
```

Рекомендую использовать **упрощенную версию** (`exportTableToExcelSimple`) - она наиболее надежная. Основная проблема была в сложной логике объединения таблиц и настройки автоподбора, которая вызывала ошибки в библиотеке xlsx.

Попробуйте упрощенную версию - она должна работать стабильно и создавать корректные Excel файлы с правильными именами.

65156
Прикрепления:
Понял проблему. Это связано с тем, как обрабатываются и кешируются данные проходки. Давайте исправим несколько ключевых моментов:

Основные проблемы:

1. Некорректное определение наличия данных проходки
2. Проблемы с кешированием данных проходки
3. Неправильная логика отображения при множественных заголовках проходки

Исправления:

1. Обновите функцию displayDrillingData():

```javascript
function displayDrillingData() {
// ПРАВИЛЬНОЕ определение наличия данных проходки
const hasHeaders = xmlData.drillingHeaders && xmlData.drillingHeaders.length > 0;
const hasDrillingData = hasHeaders && xmlData.drillingDataByHeader &&
Object.keys(xmlData.drillingDataByHeader).length > 0;

if (!hasDrillingData) {
return '<div class="no-data"><p>Нет данных проходки</p></div>';
}

let html = '<div class="section">';
html += '<h2>Данные проходки</h2>';

// ОБНОВЛЕННАЯ ЛОГИКА: Всегда устанавливаем первый заголовок по умолчанию
if (!currentDrillingHeaderId && xmlData.drillingHeaders.length > 0) {
currentDrillingHeaderId = xmlData.drillingHeaders[0].id;
}

// Если заголовков несколько, показываем таблицу выбора
if (xmlData.drillingHeaders.length > 1) {
html += `
<div class="info-group">
<div class="info-group-title">Выберите параметр скважины</div>
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table">
<thead>
<tr>
<th style="width: 50px;">ПП</th>
<th style="width: 150px;">Назначение</th>
<th style="width: 120px;">Вид скважины</th>
<th style="width: 150px;">Способ проходки</th>
<th style="width: 80px;">Забой (м)</th>
<th style="width: 120px;">Исполнитель</th>
<th style="width: 100px;">Оценка</th>
<th style="width: 100px;">Дата ввода</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>
`;

xmlData.drillingHeaders.forEach(header => {
const isSelected = currentDrillingHeaderId === header.id;
const rowClass = isSelected ? 'active' : '';

let gradeClass = '';
if (header.grade === 'Отлично') {
gradeClass = 'style="background-color: #27ae60; color: white; padding: 3px 8px; border-radius: 4px;"';
} else if (header.grade === 'Хорошо') {
gradeClass = 'style="background-color: #f39c12; color: white; padding: 3px 8px; border-radius: 4px;"';
} else if (header.grade === 'Удовлетворительно') {
gradeClass = 'style="background-color: #e67e22; color: white; padding: 3px 8px; border-radius: 4px;"';
} else if (header.grade === 'Неудовлетворительно') {
gradeClass = 'style="background-color: #e74c3c; color: white; padding: 3px 8px; border-radius: 4px;"';
}

html += `
<tr class="drilling-header-row ${rowClass}" data-header-id="${header.id}" style="cursor: pointer;">
<td style="width: 50px;">${header.number}</td>
<td style="width: 150px;">${header.purpose}</td>
<td style="width: 120px;">${header.type}</td>
<td style="width: 150px;">${header.mode}</td>
<td style="width: 80px;">${formatNumber(header.depth)}</td>
<td style="width: 120px;">${header.author}</td>
<td style="width: 100px;"><span ${gradeClass}>${header.grade || 'Не указано'}</span></td>
<td style="width: 100px;">${header.inputDate}</td>
</tr>
`;
});

html += `
</tbody>
</table>
</div>
</div>
</div>
`;
}

// Показываем данные для выбранного заголовка
if (currentDrillingHeaderId && xmlData.drillingDataByHeader[currentDrillingHeaderId]) {
const selectedHeader = xmlData.drillingHeaders.find(h => h.id === currentDrillingHeaderId);
const headerData = xmlData.drillingDataByHeader[currentDrillingHeaderId];

html += `
<div class="info-group" style="margin-top: 20px;">
<div class="info-group-title">Данные для параметра: ${selectedHeader.purpose} (ID: ${selectedHeader.id})</div>
</div>
`;

// Проверяем наличие данных для подвкладок
const hasDiameter = headerData.diameter && headerData.diameter.length > 0;
const hasIntervals = headerData.intervals && headerData.intervals.length > 0;
const hasCasing = headerData.casing && headerData.casing.length > 0;
const hasMethod = headerData.method && headerData.method.length > 0;
const hasDrillingType = headerData.drillingType && headerData.drillingType.length > 0;
const hasSludge = headerData.sludge && headerData.sludge.length > 0;
const hasConditions = headerData.conditions && headerData.conditions.length > 0;
const hasStart = headerData.start && headerData.start.length > 0;
const hasEnd = headerData.end && headerData.end.length > 0;

// Определяем первую активную подвкладку
let firstActiveSubTab = 'header';

if (hasDiameter || hasIntervals || hasCasing || hasMethod || hasDrillingType ||
hasSludge || hasConditions || hasStart || hasEnd) {

html += `<div class="inner-tabs-container">`;
html += `<div class="inner-tabs">`;

// ВКЛАДКА "ЗАГОЛОВОК" - всегда первая
html += `<div class="inner-tab ${firstActiveSubTab === 'header' ? 'active' : ''}" data-inner-tab="header">Заголовок</div>`;

// Остальные вкладки
if (hasDiameter) {
html += `<div class="inner-tab" data-inner-tab="diameter">Диаметр бурения</div>`;
}
if (hasIntervals) {
html += `<div class="inner-tab" data-inner-tab="intervals">Интервалы бурения</div>`;
}
if (hasCasing) {
html += `<div class="inner-tab" data-inner-tab="casing">Обсадка</div>`;
}
if (hasMethod) {
html += `<div class="inner-tab" data-inner-tab="method">Способ бурения</div>`;
}
if (hasDrillingType) {
html += `<div class="inner-tab" data-inner-tab="drillingType">Вид бурения</div>`;
}
if (hasSludge) {
html += `<div class="inner-tab" data-inner-tab="sludge">Способ выноски шлама</div>`;
}
if (hasConditions) {
html += `<div class="inner-tab" data-inner-tab="conditions">Условия бурения</div>`;
}
if (hasStart) {
html += `<div class="inner-tab" data-inner-tab="start">Забурка</div>`;
}
if (hasEnd) {
html += `<div class="inner-tab" data-inner-tab="end">Разбурка</div>`;
}

html += `</div>`;
html += `<div class="inner-tabs-content">`;

// СОДЕРЖИМОЕ ВКЛАДКИ "ЗАГОЛОВОК"
html += `<div id="headerInnerTab" class="inner-tab-content active">`;
html += displayDrillingHeaderForHeader(selectedHeader);
html += `</div>`;

// Остальные вкладки
if (hasDiameter) {
html += `<div id="diameterInnerTab" class="inner-tab-content">`;
html += displayDrillingDiameterForHeader(headerData.diameter);
html += `</div>`;
}
if (hasIntervals) {
html += `<div id="intervalsInnerTab" class="inner-tab-content">`;
html += displayDrillingIntervalsForHeader(headerData.intervals);
html += `</div>`;
}
if (hasCasing) {
html += `<div id="casingInnerTab" class="inner-tab-content">`;
html += displayDrillingCasingForHeader(headerData.casing);
html += `</div>`;
}
if (hasMethod) {
html += `<div id="methodInnerTab" class="inner-tab-content">`;
html += displayDrillingMethodForHeader(headerData.method);
html += `</div>`;
}
if (hasDrillingType) {
html += `<div id="drillingTypeInnerTab" class="inner-tab-content">`;
html += displayDrillingTypeForHeader(headerData.drillingType);
html += `</div>`;
}
if (hasSludge) {
html += `<div id="sludgeInnerTab" class="inner-tab-content">`;
html += displayDrillingSludgeForHeader(headerData.sludge);
html += `</div>`;
}
if (hasConditions) {
html += `<div id="conditionsInnerTab" class="inner-tab-content">`;
html += displayDrillingConditionsForHeader(headerData.conditions);
html += `</div>`;
}
if (hasStart) {
html += `<div id="startInnerTab" class="inner-tab-content">`;
html += displayDrillingStartForHeader(headerData.start);
html += `</div>`;
}
if (hasEnd) {
html += `<div id="endInnerTab" class="inner-tab-content">`;
html += displayDrillingEndForHeader(headerData.end);
html += `</div>`;
}

html += `</div>`;
html += `</div>`;
} else {
// Если нет других данных, показываем только заголовок
html += displayDrillingHeaderForHeader(selectedHeader);
}
} else {
html += '<div class="no-data"><p>Нет данных для выбранного параметра проходки</p></div>';
}

html += '</div>';
return html;
}
```

2. Добавьте обработчики для переключения заголовков проходки:

```javascript
// Добавьте эту функцию в конец файла, после всех существующих функций
function initializeDrillingHandlers() {
// Обработчики для строк выбора заголовка проходки
document.querySelectorAll('.drilling-header-row').forEach(row => {
row.addEventListener('click', function() {
const headerId = this.getAttribute('data-header-id');

// Снимаем выделение со всех строк
document.querySelectorAll('.drilling-header-row').forEach(r => {
r.classList.remove('active');
});

// Выделяем текущую строку
this.classList.add('active');

// Устанавливаем текущий заголовок
currentDrillingHeaderId = headerId;

// Перерисовываем вкладку проходки
refreshDrillingTab();
});
});

// Обработчики для внутренних вкладок проходки
document.querySelectorAll('.inner-tab').forEach(tab => {
tab.addEventListener('click', function() {
const tabName = this.getAttribute('data-inner-tab');

// Снимаем активный класс со всех вкладок
document.querySelectorAll('.inner-tab').forEach(t => {
t.classList.remove('active');
});

// Снимаем активный класс со всех контентов
document.querySelectorAll('.inner-tab-content').forEach(c => {
c.classList.remove('active');
});

// Активируем текущую вкладку и контент
this.classList.add('active');
const contentElement = document.getElementById(tabName + 'InnerTab');
if (contentElement) {
contentElement.classList.add('active');
}
});
});
}

function refreshDrillingTab() {
const wellDataElement = document.getElementById('wellData');
const currentTab = document.querySelector('.tab.active');

if (currentTab && currentTab.getAttribute('data-tab') === 'drilling') {
// Перерисовываем только вкладку проходки
const drillingContent = displayDrillingData();
const tabContent = wellDataElement.querySelector('#drillingTab');
if (tabContent) {
tabContent.innerHTML = drillingContent;
initializeDrillingHandlers();
initializeExportButtons();
}
}
}
```

3. Обновите функцию displayWellData():

Найдите функцию displayWellData() и добавьте инициализацию обработчиков после отрисовки контента:

```javascript
// В функции displayWellData(), после установки innerHTML, добавьте:
setTimeout(() => {
initializeDrillingHandlers();
initializeExportButtons();

// Другие инициализации...
initializeDocumentFilters();
initializeDocumentPreview();
}, 100);
```

4. Исправьте очистку кешей:

В функции clearCaches() добавьте сброс переменной проходки:

```javascript
function clearCaches() {
console.log('Очистка кешей...');
fileCache.clear();
wellDataCache.clear();
currentDrillingHeaderId = null; // ДОБАВЬТЕ ЭТУ СТРОКУ

if (currentMultiWellData) {
currentMultiWellData.currentXmlDoc = null;
}

if (window.gc) {
window.gc();
}
}
```

Эти исправления должны решить проблему с отображением данных проходки при переключении между скважинами. Основные изменения:

1. Корректное определение наличия данных
2. Правильная инициализация текущего заголовка
3. Динамическое обновление вкладки при переключении
4. Очистка состояния при смене скважины

Попробуйте эти изменения и проверьте, решена ли проблема.

Добавлено (2025-10-08, 08:24)
---------------------------------------------
Отлично! Исправим отображение оценки качества работ. Обновите функцию displayDrillingHeaderForHeader():

```javascript
function displayDrillingHeaderForHeader(header) {
let html = '';

// Информационная панель
html += `
<div class="doc-info-list" style="margin-bottom: 20px;">
<div class="doc-info-item">
<span class="doc-info-label">Назначение скважины:</span>
<span class="doc-info-value"><strong>${header.purpose}</strong></span>
</div>
<div class="doc-info-item">
<span class="doc-info-label">Вид скважины:</span>
<span class="doc-info-value">${header.type}</span>
</div>
<div class="doc-info-item">
<span class="doc-info-label">Способ проходки:</span>
<span class="doc-info-value">${header.mode}</span>
</div>
<div class="doc-info-item">
<span class="doc-info-label">Забой:</span>
<span class="doc-info-value">${formatNumber(header.depth)} м</span>
</div>
<div class="doc-info-item">
<span class="doc-info-label">Исполнитель:</span>
<span class="doc-info-value">${header.author}</span>
</div>
<div class="doc-info-item">
<span class="doc-info-label">Перевозка:</span>
<span class="doc-info-value">${formatNumber(header.transit)} ${header.transitUnit}</span>
</div>
<div class="doc-info-item">
<span class="doc-info-label">Дата ввода:</span>
<span class="doc-info-value">${header.inputDate}</span>
</div>
</div>
`;

// Оценка качества с компактным отображением
let gradeClass = '';
let gradeText = header.grade || 'Не указано';

if (header.grade === 'Отлично') {
gradeClass = 'style="background-color: #27ae60; color: white; padding: 6px 12px; border-radius: 4px; font-weight: 600; display: inline-block;"';
} else if (header.grade === 'Хорошо') {
gradeClass = 'style="background-color: #f39c12; color: white; padding: 6px 12px; border-radius: 4px; font-weight: 600; display: inline-block;"';
} else if (header.grade === 'Удовлетворительно') {
gradeClass = 'style="background-color: #e67e22; color: white; padding: 6px 12px; border-radius: 4px; font-weight: 600; display: inline-block;"';
} else if (header.grade === 'Неудовлетворительно') {
gradeClass = 'style="background-color: #e74c3c; color: white; padding: 6px 12px; border-radius: 4px; font-weight: 600; display: inline-block;"';
} else {
gradeClass = 'style="background-color: #95a5a6; color: white; padding: 6px 12px; border-radius: 4px; font-weight: 600; display: inline-block;"';
}

html += `
<div class="doc-info-list">
<div class="doc-info-item">
<span class="doc-info-label">Оценка качества работ:</span>
<span class="doc-info-value">
<span ${gradeClass}>${gradeText}</span>
</span>
</div>
</div>
`;

// Описание
if (header.description) {
html += `
<div class="info-group" style="margin-top: 20px;">
<div class="info-group-title">Описание</div>
<div class="description-block">
<p>${formatLongText(header.description, 100)}</p>
</div>
</div>
`;
}

return html;
}
```

Также обновите отображение оценки в таблице выбора заголовков в функции displayDrillingData():

```javascript
// В таблице выбора заголовков замените эту часть:
html += `
<td style="width: 100px;">
<span ${gradeClass} style="display: inline-block; min-width: 60px; text-align: center;">
${header.grade || 'Не указано'}
</span>
</td>
`;
```

Основные изменения:

1. Компактное отображение - добавлен display: inline-block чтобы оценка занимала только необходимую ширину
2. Фиксированные отступы - padding: 6px 12px вместо растягивания на всю ширину
3. Центрирование текста - для таблицы добавлено text-align: center
4. Минимальная ширина - min-width: 60px для сохранения читаемости

Теперь оценка качества будет отображаться как компактный цветной бейдж, а не растягиваться на всю ширину ячейки.

Добавлено (2025-10-08, 08:34)
---------------------------------------------
Это известная проблема! Функция showDirectoryPicker() работает только в безопасных контекстах (HTTPS) и локальных файлах, но не на HTTP-хостах из-за политик безопасности браузеров.

Решение:

1. Обновите функцию selectFolder():

```javascript
async function selectFolder() {
try {
// Сбрасываем состояние при выборе новой папки
clearCaches();

// ПРОВЕРКА ДОСТУПНОСТИ API
if (typeof window.showDirectoryPicker !== 'undefined') {
currentDirectoryHandle = await window.showDirectoryPicker();

// Форматируем путь в стиле директории (синхронно)
let folderPath = await formatFolderPath(currentDirectoryHandle);

document.getElementById('folderPath').innerHTML = `
<div class="folder-path-content">
<span class="folder-icon">📂</span>
<span class="folder-text">${folderPath}</span>
</div>
`;
await loadFileList();
} else {
// АЛЬТЕРНАТИВНЫЙ СПОСОБ ДЛЯ HTTP-САЙТОВ
await selectFolderFallback();
}
} catch (error) {
console.error('Ошибка выбора папки:', error);
if (error.name !== 'AbortError') {
// Показываем понятное сообщение об ошибке
if (error.name === 'SecurityError' || error.message.includes('secure context')) {
await selectFolderFallback('Для выбора папок необходимо использовать HTTPS или открывать файл локально. Используем альтернативный метод.');
} else {
alert('Ошибка при выборе папки: ' + error.message);
}
}
}
}

// АЛЬТЕРНАТИВНЫЙ МЕТОД ДЛЯ HTTP
async function selectFolderFallback(message = null) {
if (message) {
console.warn(message);
}

// Предлагаем пользователю выбрать ZIP файл вместо папки
const input = document.createElement('input');
input.type = 'file';
input.accept = '.zip';
input.multiple = true; // Разрешаем множественный выбор
input.webkitdirectory = false;

input.onchange = async (e) => {
const files = Array.from(e.target.files);
if (files.length > 0) {
clearFileList();
clearCaches();

// Создаем виртуальную "папку" из выбранных файлов
const virtualFolder = {
name: `Выбрано файлов: ${files.length}`,
files: files
};

document.getElementById('folderPath').innerHTML = `
<div class="folder-path-content">
<span class="folder-icon">📦</span>
<span class="folder-text">${virtualFolder.name}</span>
</div>
`;

// Загружаем файлы в список
await loadFilesFromFileList(files);
}
};

input.click();
}

// Функция для загрузки файлов из FileList
async function loadFilesFromFileList(files) {
const fileList = document.getElementById('fileList');
fileList.innerHTML = '';

const zipFiles = files.filter(file => file.name.toLowerCase().endsWith('.zip'));

if (zipFiles.length === 0) {
fileList.innerHTML = '<li class="file-item">Не найдено ZIP архивов</li>';
document.getElementById('fileCounter').textContent = 'Найдено архивов: 0';
return;
}

let fileCounter = 0;

zipFiles.forEach((file, index) => {
const listItem = document.createElement('li');
listItem.className = 'file-item';

const fileSize = (file.size / 1024 / 1024).toFixed(2);
const modDate = new Date(file.lastModified).toLocaleDateString();

fileCounter++;

listItem.innerHTML = `
<div class="file-number">${fileCounter}</div>
<div class="file-content">
<div class="file-name">${file.name}</div>
<div class="file-info">${fileSize} MB · ${modDate}</div>
</div>
`;

// Создаем объект, совместимый с существующей логикой
const fileObj = {
name: file.name,
handle: {
getFile: () => Promise.resolve(file)
},
size: file.size,
lastModified: file.lastModified
};

listItem.addEventListener('click', () => selectFile(fileObj, fileCounter));
fileList.appendChild(listItem);
});

// Обновляем счетчик
document.getElementById('fileCounter').textContent = `Найдено архивов: ${zipFiles.length}`;
}
```

2. Обновите функцию refreshFileList() для поддержки альтернативного метода:

```javascript
async function refreshFileList() {
if (currentDirectoryHandle) {
// Оригинальный метод с showDirectoryPicker
clearCaches();

const folderPath = await formatFolderPath(currentDirectoryHandle);
document.getElementById('folderPath').innerHTML = `
<div class="folder-path-content">
<span class="folder-icon">📂</span>
<span class="folder-text">${folderPath}</span>
</div>
`;

await loadFileList();
} else {
// Для альтернативного метода показываем сообщение
alert('Для обновления списка выберите папку или файлы заново');
}
}
```

3. Добавьте проверку при загрузке страницы:

```javascript
// В DOMContentLoaded добавьте:
document.addEventListener('DOMContentLoaded', function() {
// Проверяем доступность File System Access API
const isFSAvailable = typeof window.showDirectoryPicker !== 'undefined';

if (!isFSAvailable) {
console.log('File System Access API не доступен. Используем альтернативный метод.');
// Можно показать информационное сообщение
const folderPath = document.getElementById('folderPath');
if (folderPath) {
folderPath.innerHTML = `
<div class="folder-path-content">
<span class="folder-icon">ℹ️</span>
<span class="folder-text" style="color: #e67e22;">
Используйте "Открыть ZIP" для выбора архивов
</span>
</div>
`;
}
}

// Остальная инициализация...
});
```

4. Обновите тексты кнопок для ясности:

В HTML обновите тексты кнопок:

```html
<div class="main-actions">
<button class="action-btn primary" id="selectFolderBtn">
<span class="btn-icon">📁</span>
<span class="btn-text">Выбрать папку (локально)</span>
</button>
<button class="action-btn secondary" id="selectFileBtn">
<span class="btn-icon">📄</span>
<span class="btn-text">Открыть ZIP файл(ы)</span>
</button>
</div>
```

Преимущества этого решения:

1. Автоматическое переключение - приложение само определяет, доступен ли File System API
2. Понятные сообщения - пользователь видит, почему не работает выбор папок
3. Альтернативный метод - множественный выбор ZIP файлов работает везде
4. Совместимость - существующий код продолжает работать без изменений

Теперь приложение будет работать как на локальных файлах (с выбором папок), так и на HTTP-хостах (с выбором отдельных ZIP файлов).

Добавлено (2025-10-08, 08:38)
---------------------------------------------
Это известная проблема! Функция showDirectoryPicker() работает только в безопасных контекстах (HTTPS) и локальных файлах, но не на HTTP-хостах из-за политик безопасности браузеров.

Решение:

1. Обновите функцию selectFolder():

```javascript
async function selectFolder() {
try {
// Сбрасываем состояние при выборе новой папки
clearCaches();

// ПРОВЕРКА ДОСТУПНОСТИ API
if (typeof window.showDirectoryPicker !== 'undefined') {
currentDirectoryHandle = await window.showDirectoryPicker();

// Форматируем путь в стиле директории (синхронно)
let folderPath = await formatFolderPath(currentDirectoryHandle);

document.getElementById('folderPath').innerHTML = `
<div class="folder-path-content">
<span class="folder-icon">📂</span>
<span class="folder-text">${folderPath}</span>
</div>
`;
await loadFileList();
} else {
// АЛЬТЕРНАТИВНЫЙ СПОСОБ ДЛЯ HTTP-САЙТОВ
await selectFolderFallback();
}
} catch (error) {
console.error('Ошибка выбора папки:', error);
if (error.name !== 'AbortError') {
// Показываем понятное сообщение об ошибке
if (error.name === 'SecurityError' || error.message.includes('secure context')) {
await selectFolderFallback('Для выбора папок необходимо использовать HTTPS или открывать файл локально. Используем альтернативный метод.');
} else {
alert('Ошибка при выборе папки: ' + error.message);
}
}
}
}

// АЛЬТЕРНАТИВНЫЙ МЕТОД ДЛЯ HTTP
async function selectFolderFallback(message = null) {
if (message) {
console.warn(message);
}

// Предлагаем пользователю выбрать ZIP файл вместо папки
const input = document.createElement('input');
input.type = 'file';
input.accept = '.zip';
input.multiple = true; // Разрешаем множественный выбор
input.webkitdirectory = false;

input.onchange = async (e) => {
const files = Array.from(e.target.files);
if (files.length > 0) {
clearFileList();
clearCaches();

// Создаем виртуальную "папку" из выбранных файлов
const virtualFolder = {
name: `Выбрано файлов: ${files.length}`,
files: files
};

document.getElementById('folderPath').innerHTML = `
<div class="folder-path-content">
<span class="folder-icon">📦</span>
<span class="folder-text">${virtualFolder.name}</span>
</div>
`;

// Загружаем файлы в список
await loadFilesFromFileList(files);
}
};

input.click();
}

// Функция для загрузки файлов из FileList
async function loadFilesFromFileList(files) {
const fileList = document.getElementById('fileList');
fileList.innerHTML = '';

const zipFiles = files.filter(file => file.name.toLowerCase().endsWith('.zip'));

if (zipFiles.length === 0) {
fileList.innerHTML = '<li class="file-item">Не найдено ZIP архивов</li>';
document.getElementById('fileCounter').textContent = 'Найдено архивов: 0';
return;
}

let fileCounter = 0;

zipFiles.forEach((file, index) => {
const listItem = document.createElement('li');
listItem.className = 'file-item';

const fileSize = (file.size / 1024 / 1024).toFixed(2);
const modDate = new Date(file.lastModified).toLocaleDateString();

fileCounter++;

listItem.innerHTML = `
<div class="file-number">${fileCounter}</div>
<div class="file-content">
<div class="file-name">${file.name}</div>
<div class="file-info">${fileSize} MB · ${modDate}</div>
</div>
`;

// Создаем объект, совместимый с существующей логикой
const fileObj = {
name: file.name,
handle: {
getFile: () => Promise.resolve(file)
},
size: file.size,
lastModified: file.lastModified
};

listItem.addEventListener('click', () => selectFile(fileObj, fileCounter));
fileList.appendChild(listItem);
});

// Обновляем счетчик
document.getElementById('fileCounter').textContent = `Найдено архивов: ${zipFiles.length}`;
}
```

2. Обновите функцию refreshFileList() для поддержки альтернативного метода:

```javascript
async function refreshFileList() {
if (currentDirectoryHandle) {
// Оригинальный метод с showDirectoryPicker
clearCaches();

const folderPath = await formatFolderPath(currentDirectoryHandle);
document.getElementById('folderPath').innerHTML = `
<div class="folder-path-content">
<span class="folder-icon">📂</span>
<span class="folder-text">${folderPath}</span>
</div>
`;

await loadFileList();
} else {
// Для альтернативного метода показываем сообщение
alert('Для обновления списка выберите папку или файлы заново');
}
}
```

3. Добавьте проверку при загрузке страницы:

```javascript
// В DOMContentLoaded добавьте:
document.addEventListener('DOMContentLoaded', function() {
// Проверяем доступность File System Access API
const isFSAvailable = typeof window.showDirectoryPicker !== 'undefined';

if (!isFSAvailable) {
console.log('File System Access API не доступен. Используем альтернативный метод.');
// Можно показать информационное сообщение
const folderPath = document.getElementById('folderPath');
if (folderPath) {
folderPath.innerHTML = `
<div class="folder-path-content">
<span class="folder-icon">ℹ️</span>
<span class="folder-text" style="color: #e67e22;">
Используйте "Открыть ZIP" для выбора архивов
</span>
</div>
`;
}
}

// Остальная инициализация...
});
```

4. Обновите тексты кнопок для ясности:

В HTML обновите тексты кнопок:

```html
<div class="main-actions">
<button class="action-btn primary" id="selectFolderBtn">
<span class="btn-icon">📁</span>
<span class="btn-text">Выбрать папку (локально)</span>
</button>
<button class="action-btn secondary" id="selectFileBtn">
<span class="btn-icon">📄</span>
<span class="btn-text">Открыть ZIP файл(ы)</span>
</button>
</div>
```

Преимущества этого решения:

1. Автоматическое переключение - приложение само определяет, доступен ли File System API
2. Понятные сообщения - пользователь видит, почему не работает выбор папок
3. Альтернативный метод - множественный выбор ZIP файлов работает везде
4. Совместимость - существующий код продолжает работать без изменений

Теперь приложение будет работать как на локальных файлах (с выбором папок), так и на HTTP-хостах (с выбором отдельных ZIP файлов).

Добавлено (2025-10-08, 09:04)
---------------------------------------------
Понял проблему! Дергание высоты происходит из-за того, что содержимое вкладок разной высоты и браузер пересчитывает layout при переключении. Решим это несколькими способами:

Решение 1: Фиксированная высота контейнера

Добавьте в CSS фиксированную высоту для контейнера вкладок:

```css
/* Добавьте в секцию TAB STYLES */
.tab-content {
display: none;
min-height: 600px; /* Фиксированная минимальная высота */
overflow-y: auto;
}

.tab-content.active {
display: block;
}

/* Для внутренних вкладок проходки */
.inner-tabs-content {
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 5px 5px;
padding: 20px;
background: white;
min-height: 500px; /* Фиксированная высота */
overflow-y: auto;
}
```

Решение 2: Плавные переходы с opacity

Замените стили вкладок на версию с плавными переходами:

```css
/* ОБНОВЛЕННЫЕ СТИЛИ ВКЛАДОК */
.tab-content {
opacity: 0;
visibility: hidden;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: auto;
transition: opacity 0.3s ease, visibility 0.3s ease;
pointer-events: none;
}

.tab-content.active {
opacity: 1;
visibility: visible;
position: relative;
pointer-events: all;
min-height: 600px;
}

/* Контейнер для вкладок должен иметь относительное позиционирование */
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
background-color: white;
position: relative;
}

/* Для внутренних вкладок проходки */
.inner-tab-content {
opacity: 0;
visibility: hidden;
position: absolute;
width: 100%;
transition: opacity 0.3s ease, visibility 0.3s ease;
pointer-events: none;
}

.inner-tab-content.active {
opacity: 1;
visibility: visible;
position: relative;
pointer-events: all;
}
```

Решение 3: JavaScript для плавного переключения

Обновите логику переключения вкладок:

```javascript
// ЗАМЕНИТЕ существующие обработчики вкладок на эти:

function initializeTabs() {
// Обработчики для основных вкладок
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', function() {
const tabName = this.getAttribute('data-tab');
switchTab(tabName);
});
});

// Обработчики для внутренних вкладок проходки
document.querySelectorAll('.inner-tab').forEach(tab => {
tab.addEventListener('click', function() {
const tabName = this.getAttribute('data-inner-tab');
switchInnerTab(tabName);
});
});
}

function switchTab(tabName) {
// Снимаем активный класс со всех вкладок
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});

// Снимаем активный класс со всех контентов
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});

// Активируем текущую вкладку
const activeTab = document.querySelector(`.tab[data-tab="${tabName}"]`);
const activeContent = document.getElementById(tabName + 'Tab');

if (activeTab && activeContent) {
activeTab.classList.add('active');

// Добавляем небольшую задержку для плавности
setTimeout(() => {
activeContent.classList.add('active');
}, 50);
}
}

function switchInnerTab(tabName) {
// Снимаем активный класс со всех внутренних вкладок
document.querySelectorAll('.inner-tab').forEach(tab => {
tab.classList.remove('active');
});

// Снимаем активный класс со всех внутренних контентов
document.querySelectorAll('.inner-tab-content').forEach(content => {
content.classList.remove('active');
});

// Активируем текущую внутреннюю вкладку
const activeTab = document.querySelector(`.inner-tab[data-inner-tab="${tabName}"]`);
const activeContent = document.getElementById(tabName + 'InnerTab');

if (activeTab && activeContent) {
activeTab.classList.add('active');

// Добавляем небольшую задержку для плавности
setTimeout(() => {
activeContent.classList.add('active');
}, 50);
}
}
```

Решение 4: Универсальный CSS для стабильной высоты

Добавьте эти стили для стабилизации высоты:

```css
/* Стабилизация высоты контента */
.well-data-container {
min-height: 700px;
position: relative;
}

.tab-content-container {
min-height: 600px;
position: relative;
}

/* Гарантируем что таблицы не ломают layout */
.table-wrapper {
max-height: 500px;
min-height: 200px;
}

/* Для случаев когда данных мало */
.no-data, .loading, .error {
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}

/* Консистентные отступы */
.info-group, .section {
min-height: 100px;
}
```

Решение 5: Обновите функцию displayWellData()

```javascript
function displayWellData(fileType, fileNumber, fileName, wellId) {
// ... существующий код ...

// После установки HTML инициализируем вкладки
setTimeout(() => {
initializeTabs();
initializeDrillingHandlers();
initializeExportButtons();
initializeDocumentFilters();
initializeDocumentPreview();

// Активируем первую вкладку плавно
switchTab('primary');
}, 100);
}
```

Рекомендуемое решение:

Используйте Решение 2 + Решение 3 вместе - это даст максимально плавное переключение без дерганий.

Добавьте в ваш CSS обновленные стили вкладок и замените обработчики на новые функции. Это решит проблему с дерганием высоты при переключении вкладок.

Добавлено (2025-10-08, 09:31)
---------------------------------------------
Понял проблему! Это происходит из-за того, что при переключении вкладок не всегда правильно инициализируются внутренние вкладки и их содержимое. Давайте исправим это:

Решение 1: Улучшенная инициализация вкладок

```javascript
// ОБНОВЛЕННАЯ ФУНКЦИЯ ИНИЦИАЛИЗАЦИИ ВКЛАДОК
function initializeTabs() {
console.log('Инициализация вкладок...');

// Обработчики для основных вкладок
document.querySelectorAll('.tab').forEach(tab => {
tab.removeEventListener('click', handleTabClick); // Удаляем старые обработчики
tab.addEventListener('click', handleTabClick);
});

// Обработчики для внутренних вкладок проходки
document.querySelectorAll('.inner-tab').forEach(tab => {
tab.removeEventListener('click', handleInnerTabClick);
tab.addEventListener('click', handleInnerTabClick);
});

// Гарантируем, что активная вкладка отображает правильный контент
const activeTab = document.querySelector('.tab.active');
if (activeTab) {
const tabName = activeTab.getAttribute('data-tab');
ensureTabContentVisible(tabName);
}
}

// ОТДЕЛЬНЫЕ ФУНКЦИИ ДЛЯ ОБРАБОТЧИКОВ
function handleTabClick() {
const tabName = this.getAttribute('data-tab');
console.log('Переключение на вкладку:', tabName);
switchTab(tabName);
}

function handleInnerTabClick() {
const tabName = this.getAttribute('data-inner-tab');
console.log('Переключение на внутреннюю вкладку:', tabName);
switchInnerTab(tabName);
}

// ОБНОВЛЕННАЯ ФУНКЦИЯ ПЕРЕКЛЮЧЕНИЯ ВКЛАДОК
function switchTab(tabName) {
console.log('Активация вкладки:', tabName);

// Снимаем активный класс со всех вкладок
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});

// Снимаем активный класс со всех контентов
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});

// Активируем текущую вкладку
const activeTab = document.querySelector(`.tab[data-tab="${tabName}"]`);
const activeContent = document.getElementById(tabName + 'Tab');

if (activeTab && activeContent) {
activeTab.classList.add('active');
activeContent.classList.add('active');

// ДОПОЛНИТЕЛЬНАЯ ИНИЦИАЛИЗАЦИЯ ДЛЯ КОНКРЕТНЫХ ВКЛАДОК
initializeTabSpecificContent(tabName);

console.log('Вкладка активирована:', tabName);
} else {
console.error('Не найдена вкладка или контент:', tabName);
}
}

// ОБНОВЛЕННАЯ ФУНКЦИЯ ДЛЯ ВНУТРЕННИХ ВКЛАДОК
function switchInnerTab(tabName) {
console.log('Активация внутренней вкладки:', tabName);

// Снимаем активный класс со всех внутренних вкладок
document.querySelectorAll('.inner-tab').forEach(tab => {
tab.classList.remove('active');
});

// Снимаем активный класс со всех внутренних контентов
document.querySelectorAll('.inner-tab-content').forEach(content => {
content.classList.remove('active');
});

// Активируем текущую внутреннюю вкладку
const activeTab = document.querySelector(`.inner-tab[data-inner-tab="${tabName}"]`);
const activeContent = document.getElementById(tabName + 'InnerTab');

if (activeTab && activeContent) {
activeTab.classList.add('active');
activeContent.classList.add('active');

console.log('Внутренняя вкладка активирована:', tabName);
}
}

// ФУНКЦИЯ ДЛЯ ГАРАНТИИ ВИДИМОСТИ КОНТЕНТА ВКЛАДКИ
function ensureTabContentVisible(tabName) {
const content = document.getElementById(tabName + 'Tab');
const tab = document.querySelector(`.tab[data-tab="${tabName}"]`);

if (content && tab) {
// Сначала скрываем все
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));

// Потом показываем нужные
content.classList.add('active');
tab.classList.add('active');

initializeTabSpecificContent(tabName);
}
}

// ФУНКЦИЯ ДЛЯ ИНИЦИАЛИЗАЦИИ КОНТЕНТА КОНКРЕТНОЙ ВКЛАДКИ
function initializeTabSpecificContent(tabName) {
console.log('Инициализация контента для вкладки:', tabName);

switch(tabName) {
case 'primary':
case 'final':
case 'gis':
// Для вкладок документирования - инициализируем внутренние вкладки
initializeDocumentationInnerTabs(tabName);
break;
case 'drilling':
// Для проходки - инициализируем обработчики
initializeDrillingHandlers();
break;
case 'documents':
// Для документов - инициализируем фильтры
initializeDocumentFilters();
initializeDocumentPreview();
break;
case 'assays':
// Для опробования - инициализируем экспорт
initializeExportButtons();
break;
case 'coreboxes':
// Для керновых ящиков - инициализируем предпросмотр
initializeDocumentPreview();
break;
}
}

// ФУНКЦИЯ ДЛЯ ИНИЦИАЛИЗАЦИИ ВНУТРЕННИХ ВКЛАДОК ДОКУМЕНТИРОВАНИЯ
function initializeDocumentationInnerTabs(parentTab) {
console.log('Инициализация внутренних вкладок для:', parentTab);

const container = document.getElementById(parentTab + 'Tab');
if (!container) return;

// Находим все внутренние вкладки в этом контейнере
const innerTabs = container.querySelectorAll('.inner-tab');
const innerContents = container.querySelectorAll('.inner-tab-content');

if (innerTabs.length > 0) {
console.log('Найдено внутренних вкладок:', innerTabs.length);

// Удаляем старые обработчики
innerTabs.forEach(tab => {
tab.removeEventListener('click', handleInnerTabClick);
tab.addEventListener('click', handleInnerTabClick);
});

// Гарантируем, что первая вкладка активна
const firstTab = innerTabs[0];
const firstTabName = firstTab.getAttribute('data-inner-tab');

if (firstTab && !container.querySelector('.inner-tab.active')) {
firstTab.classList.add('active');
const firstContent = container.querySelector(`#${firstTabName}InnerTab`);
if (firstContent) {
firstContent.classList.add('active');
}
}
}
}
```

Решение 2: Обновите функцию displayWellData()

```javascript
function displayWellData(fileType, fileNumber, fileName, wellId) {
console.log('Отображение данных скважины...');

// ... существующий код подготовки данных ...

// После установки HTML
setTimeout(() => {
console.log('Инициализация интерфейса...');

// ПЕРВОНАЧАЛЬНАЯ ИНИЦИАЛИЗАЦИЯ ВСЕГО
initializeTabs();
initializeDrillingHandlers();
initializeExportButtons();
initializeDocumentFilters();
initializeDocumentPreview();

// ГАРАНТИРУЕМ, ЧТО ПЕРВАЯ ВКЛАДКА ПОЛНОСТЬЮ ИНИЦИАЛИЗИРОВАНА
const firstTab = document.querySelector('.tab');
if (firstTab) {
const firstTabName = firstTab.getAttribute('data-tab');
console.log('Активация первой вкладки:', firstTabName);

// Принудительно активируем первую вкладку
switchTab(firstTabName);
}

}, 150); // Увеличиваем задержку для полной отрисовки DOM
}
```

Решение 3: Улучшенные CSS стилы для надежности

```css
/* ГАРАНТИЯ ВИДИМОСТИ АКТИВНЫХ ВКЛАДОК */
.tab-content:not(.active) {
display: none !important;
}

.tab-content.active {
display: block !important;
}

.inner-tab-content:not(.active) {
display: none !important;
}

.inner-tab-content.active {
display: block !important;
}

/* Убираем возможные конфликты transition */
.tab-content,
.inner-tab-content {
transition: none; /* Убираем transition если они мешают */
}
```

Решение 4: Добавьте принудительное обновление при клике

```javascript
// ДОБАВЬТЕ эту функцию для принудительного обновления
function refreshActiveTab() {
const activeTab = document.querySelector('.tab.active');
if (activeTab) {
const tabName = activeTab.getAttribute('data-tab');
console.log('Принудительное обновление вкладки:', tabName);
initializeTabSpecificContent(tabName);
}
}

// Можно вызвать эту функцию если данные не отображаются
// Например, добавить кнопку "Обновить" или вызывать при определенных условиях
```

Основные изменения:

1. Гарантированная активация - принудительно активируем первую вкладку при загрузке
2. Отдельные обработчики - убираем конфликты между обработчиками
3. Конкретная инициализация - для каждой вкладки свой набор инициализаций
4. Улучшенная диагностика - больше console.log для отладки
5. Удаление старых обработчиков - предотвращает накопление дублирующихся обработчиков

Теперь при переключении вкладок данные должны отображаться сразу, без необходимости переключать внутренние вкладки.

Добавлено (2025-10-08, 09:41)
---------------------------------------------
Понял проблему! Это происходит из-за того, что при переключении вкладок не всегда правильно инициализируются внутренние вкладки и их содержимое. Давайте исправим это:

Решение 1: Улучшенная инициализация вкладок

```javascript
// ОБНОВЛЕННАЯ ФУНКЦИЯ ИНИЦИАЛИЗАЦИИ ВКЛАДОК
function initializeTabs() {
console.log('Инициализация вкладок...');

// Обработчики для основных вкладок
document.querySelectorAll('.tab').forEach(tab => {
tab.removeEventListener('click', handleTabClick); // Удаляем старые обработчики
tab.addEventListener('click', handleTabClick);
});

// Обработчики для внутренних вкладок проходки
document.querySelectorAll('.inner-tab').forEach(tab => {
tab.removeEventListener('click', handleInnerTabClick);
tab.addEventListener('click', handleInnerTabClick);
});

// Гарантируем, что активная вкладка отображает правильный контент
const activeTab = document.querySelector('.tab.active');
if (activeTab) {
const tabName = activeTab.getAttribute('data-tab');
ensureTabContentVisible(tabName);
}
}

// ОТДЕЛЬНЫЕ ФУНКЦИИ ДЛЯ ОБРАБОТЧИКОВ
function handleTabClick() {
const tabName = this.getAttribute('data-tab');
console.log('Переключение на вкладку:', tabName);
switchTab(tabName);
}

function handleInnerTabClick() {
const tabName = this.getAttribute('data-inner-tab');
console.log('Переключение на внутреннюю вкладку:', tabName);
switchInnerTab(tabName);
}

// ОБНОВЛЕННАЯ ФУНКЦИЯ ПЕРЕКЛЮЧЕНИЯ ВКЛАДОК
function switchTab(tabName) {
console.log('Активация вкладки:', tabName);

// Снимаем активный класс со всех вкладок
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});

// Снимаем активный класс со всех контентов
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});

// Активируем текущую вкладку
const activeTab = document.querySelector(`.tab[data-tab="${tabName}"]`);
const activeContent = document.getElementById(tabName + 'Tab');

if (activeTab && activeContent) {
activeTab.classList.add('active');
activeContent.classList.add('active');

// ДОПОЛНИТЕЛЬНАЯ ИНИЦИАЛИЗАЦИЯ ДЛЯ КОНКРЕТНЫХ ВКЛАДОК
initializeTabSpecificContent(tabName);

console.log('Вкладка активирована:', tabName);
} else {
console.error('Не найдена вкладка или контент:', tabName);
}
}

// ОБНОВЛЕННАЯ ФУНКЦИЯ ДЛЯ ВНУТРЕННИХ ВКЛАДОК
function switchInnerTab(tabName) {
console.log('Активация внутренней вкладки:', tabName);

// Снимаем активный класс со всех внутренних вкладок
document.querySelectorAll('.inner-tab').forEach(tab => {
tab.classList.remove('active');
});

// Снимаем активный класс со всех внутренних контентов
document.querySelectorAll('.inner-tab-content').forEach(content => {
content.classList.remove('active');
});

// Активируем текущую внутреннюю вкладку
const activeTab = document.querySelector(`.inner-tab[data-inner-tab="${tabName}"]`);
const activeContent = document.getElementById(tabName + 'InnerTab');

if (activeTab && activeContent) {
activeTab.classList.add('active');
activeContent.classList.add('active');

console.log('Внутренняя вкладка активирована:', tabName);
}
}

// ФУНКЦИЯ ДЛЯ ГАРАНТИИ ВИДИМОСТИ КОНТЕНТА ВКЛАДКИ
function ensureTabContentVisible(tabName) {
const content = document.getElementById(tabName + 'Tab');
const tab = document.querySelector(`.tab[data-tab="${tabName}"]`);

if (content && tab) {
// Сначала скрываем все
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));

// Потом показываем нужные
content.classList.add('active');
tab.classList.add('active');

initializeTabSpecificContent(tabName);
}
}

// ФУНКЦИЯ ДЛЯ ИНИЦИАЛИЗАЦИИ КОНТЕНТА КОНКРЕТНОЙ ВКЛАДКИ
function initializeTabSpecificContent(tabName) {
console.log('Инициализация контента для вкладки:', tabName);

switch(tabName) {
case 'primary':
case 'final':
case 'gis':
// Для вкладок документирования - инициализируем внутренние вкладки
initializeDocumentationInnerTabs(tabName);
break;
case 'drilling':
// Для проходки - инициализируем обработчики
initializeDrillingHandlers();
break;
case 'documents':
// Для документов - инициализируем фильтры
initializeDocumentFilters();
initializeDocumentPreview();
break;
case 'assays':
// Для опробования - инициализируем экспорт
initializeExportButtons();
break;
case 'coreboxes':
// Для керновых ящиков - инициализируем предпросмотр
initializeDocumentPreview();
break;
}
}

// ФУНКЦИЯ ДЛЯ ИНИЦИАЛИЗАЦИИ ВНУТРЕННИХ ВКЛАДОК ДОКУМЕНТИРОВАНИЯ
function initializeDocumentationInnerTabs(parentTab) {
console.log('Инициализация внутренних вкладок для:', parentTab);

const container = document.getElementById(parentTab + 'Tab');
if (!container) return;

// Находим все внутренние вкладки в этом контейнере
const innerTabs = container.querySelectorAll('.inner-tab');
const innerContents = container.querySelectorAll('.inner-tab-content');

if (innerTabs.length > 0) {
console.log('Найдено внутренних вкладок:', innerTabs.length);

// Удаляем старые обработчики
innerTabs.forEach(tab => {
tab.removeEventListener('click', handleInnerTabClick);
tab.addEventListener('click', handleInnerTabClick);
});

// Гарантируем, что первая вкладка активна
const firstTab = innerTabs[0];
const firstTabName = firstTab.getAttribute('data-inner-tab');

if (firstTab && !container.querySelector('.inner-tab.active')) {
firstTab.classList.add('active');
const firstContent = container.querySelector(`#${firstTabName}InnerTab`);
if (firstContent) {
firstContent.classList.add('active');
}
}
}
}
```

Решение 2: Обновите функцию displayWellData()

```javascript
function displayWellData(fileType, fileNumber, fileName, wellId) {
console.log('Отображение данных скважины...');

// ... существующий код подготовки данных ...

// После установки HTML
setTimeout(() => {
console.log('Инициализация интерфейса...');

// ПЕРВОНАЧАЛЬНАЯ ИНИЦИАЛИЗАЦИЯ ВСЕГО
initializeTabs();
initializeDrillingHandlers();
initializeExportButtons();
initializeDocumentFilters();
initializeDocumentPreview();

// ГАРАНТИРУЕМ, ЧТО ПЕРВАЯ ВКЛАДКА ПОЛНОСТЬЮ ИНИЦИАЛИЗИРОВАНА
const firstTab = document.querySelector('.tab');
if (firstTab) {
const firstTabName = firstTab.getAttribute('data-tab');
console.log('Активация первой вкладки:', firstTabName);

// Принудительно активируем первую вкладку
switchTab(firstTabName);
}

}, 150); // Увеличиваем задержку для полной отрисовки DOM
}
```

Решение 3: Улучшенные CSS стилы для надежности

```css
/* ГАРАНТИЯ ВИДИМОСТИ АКТИВНЫХ ВКЛАДОК */
.tab-content:not(.active) {
display: none !important;
}

.tab-content.active {
display: block !important;
}

.inner-tab-content:not(.active) {
display: none !important;
}

.inner-tab-content.active {
display: block !important;
}

/* Убираем возможные конфликты transition */
.tab-content,
.inner-tab-content {
transition: none; /* Убираем transition если они мешают */
}
```

Решение 4: Добавьте принудительное обновление при клике

```javascript
// ДОБАВЬТЕ эту функцию для принудительного обновления
function refreshActiveTab() {
const activeTab = document.querySelector('.tab.active');
if (activeTab) {
const tabName = activeTab.getAttribute('data-tab');
console.log('Принудительное обновление вкладки:', tabName);
initializeTabSpecificContent(tabName);
}
}

// Можно вызвать эту функцию если данные не отображаются
// Например, добавить кнопку "Обновить" или вызывать при определенных условиях
```

Основные изменения:

1. Гарантированная активация - принудительно активируем первую вкладку при загрузке
2. Отдельные обработчики - убираем конфликты между обработчиками
3. Конкретная инициализация - для каждой вкладки свой набор инициализаций
4. Улучшенная диагностика - больше console.log для отладки
5. Удаление старых обработчиков - предотвращает накопление дублирующихся обработчиков

Теперь при переключении вкладок данные должны отображаться сразу, без необходимости переключать внутренние вкладки.

**ПОЛНОЕ РЕШЕНИЕ ОПТИМИЗАЦИИ КОДА**

## 1. УДАЛЯЕМ ДУБЛИРУЮЩИЕСЯ ФУНКЦИИ

### Удалить эти функции полностью:

```javascript
// НАЙТИ И УДАЛИТЬ следующие функции:

// 1. Дублирует formatLongText
function safeTextDisplay(text, maxLength = 50) {
if (!text) return 'не указано';

try {
const decodedText = decodeURIComponent(escape(text));

if (decodedText.length > maxLength) {
return decodedText.substring(0, maxLength) + '...';
}

return decodedText;
} catch (error) {
console.warn('Не удалось декодировать текст:', text);
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
}
}

// 2. Не используется
function analyzeDocumentLinks(xmlContent, wellId) {
const documentLinks = new Map();
// ... весь код функции
}

// 3. Дублирует логику из formatWellDisplayName
function findWellInfoById(wellId, xmlContent) {
// ... весь код функции
}

// 4. Избыточная функция
function getDocumentTooltip(doc) {
const parts = [];
// ... весь код функции
}
```

### ЗАМЕНИТЬ ВЫЗОВЫ:
Во всем коде заменить `safeTextDisplay(text)` на `formatLongText(text)`

## 2. ОБЪЕДИНЯЕМ ФУНКЦИИ ОТОБРАЖЕНИЯ ТАБЛИЦ

### Удаляем 9 отдельных функций и заменяем одной:

**УДАЛИТЬ:**
```javascript
function displayDrillingDiameterForHeader(diameterData) { ... }
function displayDrillingIntervalsForHeader(intervalsData) { ... }
function displayDrillingCasingForHeader(casingData) { ... }
function displayDrillingMethodForHeader(methodData) { ... }
function displayDrillingTypeForHeader(typeData) { ... }
function displayDrillingSludgeForHeader(sludgeData) { ... }
function displayDrillingConditionsForHeader(conditionsData) { ... }
function displayDrillingStartForHeader(startData) { ... }
function displayDrillingEndForHeader(endData) { ... }
```

**ДОБАВИТЬ ВМЕСТО НИХ:**

```javascript
// УНИВЕРСАЛЬНАЯ ФУНКЦИЯ ДЛЯ ВСЕХ ТАБЛИЦ ПРОХОДКИ
function displayDrillingTableForHeader(data, config) {
if (!data || data.length === 0) {
return '<div class="no-data"><p>Нет данных</p></div>';
}

const { tableId, fileName, columns } = config;

let html = createExportButton(tableId, fileName);

html += `
<div class="table-wrapper" id="${tableId}">
<div class="table-header">
<table class="interval-table">
<thead>
<tr>
${columns.map(col =>
`<th style="width: ${col.width};">${col.title}</th>`
).join('')}
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>
`;

data.forEach(item => {
html += '<tr>';
columns.forEach(col => {
const value = col.formatter ? col.formatter(item[col.field]) : item[col.field];
html += `<td style="width: ${col.width};">${value}</td>`;
});
html += '</tr>';
});

html += `
</tbody>
</table>
</div>
</div>
`;

return html;
}

// КОНФИГУРАЦИЯ ДЛЯ КАЖДОГО ТИПА ТАБЛИЦ
const drillingTableConfigs = {
diameter: {
tableId: 'drillingDiameterTable',
fileName: 'Диаметр_бурения',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'diameter', title: 'Диаметр (мм)', width: '100px' },
{ field: 'bitType', title: 'Тип коронки', width: '150px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},
intervals: {
tableId: 'drillingIntervalsTable',
fileName: 'Интервалы_бурения',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'coreOutput', title: 'Выход керна, %', width: '100px', formatter: formatNumber },
{ field: 'coreDiameter', title: 'Диаметр керна, мм', width: '100px', formatter: formatNumber },
{ field: 'coreLength', title: 'Длина керна, м', width: '100px', formatter: formatNumber },
{ field: 'description', title: 'Описание', width: 'auto' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
}
// Добавить конфиги для остальных типов...
};
```

### ИСПРАВИТЬ ВЫЗОВЫ В `displayDrillingData()`:

**БЫЛО:**
```javascript
if (hasDiameter) {
html += `<div id="diameterInnerTab" class="inner-tab-content">`;
html += displayDrillingDiameterForHeader(headerData.diameter);
html += `</div>`;
}
```

**СТАЛО:**
```javascript
if (hasDiameter) {
html += `<div id="diameterInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.diameter, drillingTableConfigs.diameter);
html += `</div>`;
}
```

## 3. УПРОЩАЕМ ОБРАБОТЧИКИ СОБЫТИЙ

### Удаляем множественные addEventListener:

**УДАЛИТЬ из `displayWellData()`:**
```javascript
// УДАЛИТЬ весь этот блок:
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', function() {
const tabName = this.getAttribute('data-tab');
switchMainTab(tabName);
});
});

document.querySelectorAll('.inner-tab').forEach(innerTab => {
innerTab.addEventListener('click', function() {
const innerTabName = this.getAttribute('data-inner-tab');
switchInnerTab(innerTabName);
});
});

document.querySelectorAll('.drilling-header-row').forEach(row => {
row.addEventListener('click', function() {
const headerId = this.getAttribute('data-header-id');
selectDrillingHeader(headerId);
});
});
```

**ДОБАВИТЬ В НАЧАЛО ФАЙЛА (после глобальных переменных):**

```javascript
// УНИВЕРСАЛЬНЫЙ ОБРАБОТЧИК СОБЫТИЙ
document.addEventListener('click', function(e) {
// Обработка основных вкладок
if (e.target.classList.contains('tab')) {
switchMainTab(e.target.getAttribute('data-tab'));
return;
}

// Обработка внутренних вкладок
if (e.target.classList.contains('inner-tab')) {
switchInnerTab(e.target.getAttribute('data-inner-tab'));
return;
}

// Обработка выбора заголовка проходки
if (e.target.classList.contains('drilling-header-row')) {
selectDrillingHeader(e.target.getAttribute('data-header-id'));
return;
}

// Обработка кнопок документов
if (e.target.classList.contains('preview-doc-btn') ||
e.target.classList.contains('corebox-preview-btn')) {
const docId = e.target.getAttribute('data-doc-id');
const fileName = e.target.getAttribute('data-file-name');
const fileType = e.target.getAttribute('data-file-type');
const isImage = e.target.getAttribute('data-is-image');
const blobPath = e.target.getAttribute('data-blob-path');
previewDocument(docId, fileName, fileType, isImage, blobPath);
return;
}

if (e.target.classList.contains('download-doc-btn') ||
e.target.classList.contains('corebox-download-btn') ||
e.target.classList.contains('download-from-modal')) {
const docId = e.target.getAttribute('data-doc-id');
const fileName = e.target.getAttribute('data-file-name');
const blobPath = e.target.getAttribute('data-blob-path');
downloadDocument(docId, fileName, blobPath);
return;
}

// Обработка кнопок экспорта
if (e.target.classList.contains('export-excel-btn')) {
const tableId = e.target.getAttribute('data-table-id');
const fileName = e.target.getAttribute('data-file-name');
const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper');
if (tableWrapper) {
exportTableToExcelSimple(tableWrapper, fileName);
}
return;
}
});
```

**УДАЛИТЬ ФУНКЦИИ:**
```javascript
function initializeDocumentButtons() {
// УДАЛИТЬ всю функцию - теперь обработка в универсальном обработчике
}

// УДАЛИТЬ отдельные обработчики в previewDocument и downloadDocument
```

## 4. УБИРАЕМ ИЗБЫТОЧНОЕ ЛОГИРОВАНИЕ

### УДАЛИТЬ ВО ВСЕМ КОДЕ:

```javascript
// УДАЛИТЬ такие console.log:
console.log('=== ПОИСК ДОКУМЕНТОВ ДЛЯ СКВАЖИНЫ ${wellId} ===');
console.log('Создана карта DOCUM_DCAA: ${documentInfoMap.size} записей');
console.log('=== РЕЗУЛЬТАТЫ ИЗВЛЕЧЕНИЯ ===');
console.log('=== PREVIEW DOCUMENT START ===');
console.log('=== PREVIEW DOCUMENT END ===');
console.log('=== СВЯЗЫВАНИЕ ДОКУМЕНТОВ С ЯЩИКАМИ ===');
console.log('=== СВОДКА СВЯЗЕЙ ===');

// УДАЛИТЬ подробные логи в циклах:
console.log(`✓ Документ "${displayName}" принадлежит скважине: ${wellInfo.displayName}`);
console.log(`✗ Документ "${displayName}" не принадлежит скважине ${wellId}`);
console.log(`? Документ "${displayName}" не имеет связи с объектом`);

// ОСТАВИТЬ ТОЛЬКО:
console.error() - для ошибок
console.warn() - для предупреждений
console.log() - только для ключевых этапов (начало/конец загрузки)
```

### ОПТИМИЗИРОВАННАЯ ВЕРСИЯ `extractAttachedDocumentsFast`:

**БЫЛО:**
```javascript
console.log(`=== ПОИСК ДОКУМЕНТОВ ДЛЯ СКВАЖИНЫ ${wellId} ===`);
// много console.log внутри
console.log(`=== РЕЗУЛЬТАТЫ ИЗВЛЕЧЕНИЯ ===`);
```

**СТАЛО:**
```javascript
console.log(`Поиск документов для скважины ${wellId}`);
// только критичные логи
console.log(`Найдено документов: ${documents.length}`);
```

## 5. ОБЪЕДИНЯЕМ ФУНКЦИИ ФОРМАТИРОВАНИЯ

**УДАЛИТЬ:**
```javascript
// Удалить отдельные функции форматирования если они простые
```

**ДОБАВИТЬ:**
```javascript
// УНИВЕРСАЛЬНЫЙ ФОРМАТТЕР
function formatValue(value, type = 'text') {
if (!value || value === 'Не указано') return 'Не указано';

switch(type) {
case 'number':
return formatNumber(value);
case 'date':
return formatDate(value);
case 'text':
return formatLongText(value);
case 'wellName':
return formatWellDisplayName(value);
default:
return value;
}
}
```

## 6. УПРОЩАЕМ СИСТЕМУ КЕШИРОВАНИЯ

**ЗАМЕНИТЬ:**
```javascript
// БЫЛО:
let fileCache = new Map();
let wellDataCache = new Map();

// СТАЛО:
const dataCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 минут

function getCachedData(key) {
const item = dataCache.get(key);
if (item && Date.now() - item.timestamp < CACHE_TTL) {
return item.data;
}
dataCache.delete(key);
return null;
}

function setCachedData(key, data) {
dataCache.set(key, {
data: data,
timestamp: Date.now()
});
}
```

**ОБНОВИТЬ ВЫЗОВЫ:**
```javascript
// БЫЛО:
const cachedData = getCachedWellData(wellId);
setCachedWellData(wellId, wellData);

// СТАЛО:
const cachedData = getCachedData(`well_${wellId}`);
setCachedData(`well_${wellId}`, wellData);
```

## 7. УПРОЩАЕМ ФУНКЦИИ РАБОТЫ С ZIP

**ОБЪЕДИНИТЬ:**
```javascript
// УДАЛИТЬ:
function getZipFileNames(zip) { ... }
function safeFileName(filename) { ... }

// ДОБАВИТЬ ВМЕСТО НИХ:
function getZipFiles(zip) {
const files = [];
for (const filename in zip.files) {
const zipEntry = zip.files[filename];
if (zipEntry.dir || filename.endsWith('/')) continue;

// Упрощенное получение имени файла
let correctedName = filename;
try {
correctedName = decodeURIComponent(escape(filename));
} catch (error) {
// оставляем оригинальное имя
}

files.push({
originalName: filename,
correctedName: correctedName,
entry: zipEntry
});
}
return files;
}
```

## 8. ФИНАЛЬНАЯ ПРОВЕРКА - УДАЛИТЬ НЕИСПОЛЬЗУЕМЫЕ ПЕРЕМЕННЫЕ

**УДАЛИТЬ ИЗ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ:**
```javascript
// УДАЛИТЬ:
let keepAliveInterval = null;
let lastActivityTime = Date.now();
let currentLoadingOperation = null;
let lastSelectedFileId = null;
let lastSelectedFileName = null;

// УДАЛИТЬ функции:
function startKeepAlive() { ... }
function stopKeepAlive() { ... }
function startLongOperationProgress() { ... }
function cancelCurrentOperation() { ... }
```

## РЕЗУЛЬТАТ:

После этих изменений код станет:
- **На 25-30% короче** (удалено ~500-700 строк)
- **Более читаемым** (унифицированные функции)
- **Легче в поддержке** (меньше дублирования)
- **Такой же функциональности** (вся логика сохранена)

Хотите, чтобы я показал полный код оптимизированных функций?

Добавлено (2025-10-08, 11:21)
---------------------------------------------
**ПОЛНАЯ ФУНКЦИЯ `displayDrillingTableForHeader`:**

```javascript
// УНИВЕРСАЛЬНАЯ ФУНКЦИЯ ДЛЯ ВСЕХ ТАБЛИЦ ПРОХОДКИ
function displayDrillingTableForHeader(data, config) {
if (!data || data.length === 0) {
return '<div class="no-data"><p>Нет данных</p></div>';
}

const { tableId, fileName, columns, extraHtml = '' } = config;

let html = createExportButton(tableId, fileName);

html += `
<div class="table-wrapper" id="${tableId}">
<div class="table-header">
<table class="interval-table">
<thead>
<tr>
${columns.map(col =>
`<th style="width: ${col.width};">${col.title}</th>`
).join('')}
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>
`;

data.forEach(item => {
html += '<tr>';
columns.forEach(col => {
let value = item[col.field];

// Применяем форматтер если есть
if (col.formatter) {
value = col.formatter(value, item);
} else if (typeof value === 'number') {
value = formatNumber(value);
}

html += `<td style="width: ${col.width};" title="${value}">${value || ''}</td>`;
});
html += '</tr>';
});

html += `
</tbody>
</table>
</div>
</div>
${extraHtml}
`;

return html;
}

// КОНФИГУРАЦИЯ ДЛЯ ВСЕХ ТИПОВ ТАБЛИЦ ПРОХОДКИ
const drillingTableConfigs = {
diameter: {
tableId: 'drillingDiameterTable',
fileName: 'Диаметр_бурения',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'diameter', title: 'Диаметр (мм)', width: '100px' },
{ field: 'bitType', title: 'Тип коронки', width: '150px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

intervals: {
tableId: 'drillingIntervalsTable',
fileName: 'Интервалы_бурения',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'coreOutput', title: 'Выход керна, %', width: '100px', formatter: formatNumber },
{ field: 'coreDiameter', title: 'Диаметр керна, мм', width: '100px', formatter: formatNumber },
{ field: 'coreLength', title: 'Длина керна, м', width: '100px', formatter: formatNumber },
{ field: 'description', title: 'Описание', width: '200px', formatter: (val) => `<div class="description-text">${val}</div>` },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

casing: {
tableId: 'drillingCasingTable',
fileName: 'Обсадка',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'pipeType', title: 'Тип трубы', width: '120px' },
{ field: 'diameter', title: 'Диаметр (мм)', width: '100px' },
{ field: 'extraction', title: 'Извлечение', width: '100px', formatter: (val) => val === 'Д' ? 'Да' : val === 'Н' ? 'Нет' : val },
{ field: 'wallThickness', title: 'Толщина стенки, мм', width: '100px', formatter: formatNumber },
{ field: 'filterType', title: 'Тип фильтра', width: '120px' },
{ field: 'perforation', title: 'Способ перфорации', width: '120px' },
{ field: 'wellness', title: 'Скважность, %', width: '100px', formatter: formatNumber },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

method: {
tableId: 'drillingMethodTable',
fileName: 'Способ_бурения',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'method', title: 'Способ бурения', width: '200px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

drillingType: {
tableId: 'drillingTypeTable',
fileName: 'Вид_бурения',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'drillingType', title: 'Вид бурения', width: '150px' },
{ field: 'rigType', title: 'Тип бурового станка', width: '150px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

sludge: {
tableId: 'drillingSludgeTable',
fileName: 'Способ_выноски_шлама',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'sludgeMethod', title: 'Способ выноски шлама', width: '200px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

conditions: {
tableId: 'drillingConditionsTable',
fileName: 'Условия_бурения',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'conditions', title: 'Условия бурения', width: '200px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

start: {
tableId: 'drillingStartTable',
fileName: 'Забурка',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'diameter', title: 'Диаметр (мм)', width: '100px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

end: {
tableId: 'drillingEndTable',
fileName: 'Разбурка',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'diameter', title: 'Диаметр (мм)', width: '100px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
}
};

// СПЕЦИАЛЬНАЯ ФУНКЦИЯ ДЛЯ ОБСАДКИ С ДОПОЛНИТЕЛЬНОЙ ИНФОРМАЦИЕЙ
function displayDrillingCasingWithHoles(casingData) {
const config = drillingTableConfigs.casing;
const casingWithHoles = casingData.filter(c => c.holeLength > 0 || c.holeWidth > 0);

let extraHtml = '';
if (casingWithHoles.length > 0) {
extraHtml = `
<div class="info-group" style="margin-top: 20px;">
<div class="info-group-title">Размеры отверстий фильтров</div>
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table">
<thead>
<tr>
<th style="width: 50px;">ПП</th>
<th style="width: 150px;">Интервал</th>
<th style="width: 150px;">Длина отверстия, мм</th>
<th style="width: 150px;">Ширина отверстия, мм</th>
<th style="width: 150px;">Оборудование низа колонны</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>
`;

casingWithHoles.forEach(casing => {
extraHtml += `
<tr>
<td style="width: 50px;">${casing.number}</td>
<td style="width: 150px;">${formatNumber(casing.from)} - ${formatNumber(casing.to)} м</td>
<td style="width: 150px;">${formatNumber(casing.holeLength)}</td>
<td style="width: 150px;">${formatNumber(casing.holeWidth)}</td>
<td style="width: 150px;">${casing.bottomEquipment}</td>
</tr>
`;
});

extraHtml += `
</tbody>
</table>
</div>
</div>
</div>
`;
}

return displayDrillingTableForHeader(casingData, {
...config,
extraHtml: extraHtml
});
}
```

**ТЕПЕРЬ В `displayDrillingData()` ЗАМЕНЯЕМ ВСЕ ВЫЗОВЫ:**

```javascript
// В функции displayDrillingData() НАЙТИ И ЗАМЕНИТЬ:

// БЫЛО:
if (hasDiameter) {
html += `<div id="diameterInnerTab" class="inner-tab-content">`;
html += displayDrillingDiameterForHeader(headerData.diameter);
html += `</div>`;
}

// СТАЛО:
if (hasDiameter) {
html += `<div id="diameterInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.diameter, drillingTableConfigs.diameter);
html += `</div>`;
}

// БЫЛО:
if (hasIntervals) {
html += `<div id="intervalsInnerTab" class="inner-tab-content">`;
html += displayDrillingIntervalsForHeader(headerData.intervals);
html += `</div>`;
}

// СТАЛО:
if (hasIntervals) {
html += `<div id="intervalsInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.intervals, drillingTableConfigs.intervals);
html += `</div>`;
}

// БЫЛО:
if (hasCasing) {
html += `<div id="casingInnerTab" class="inner-tab-content">`;
html += displayDrillingCasingForHeader(headerData.casing);
html += `</div>`;
}

// СТАЛО:
if (hasCasing) {
html += `<div id="casingInnerTab" class="inner-tab-content">`;
html += displayDrillingCasingWithHoles(headerData.casing);
html += `</div>`;
}

// БЫЛО:
if (hasMethod) {
html += `<div id="methodInnerTab" class="inner-tab-content">`;
html += displayDrillingMethodForHeader(headerData.method);
html += `</div>`;
}

// СТАЛО:
if (hasMethod) {
html += `<div id="methodInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.method, drillingTableConfigs.method);
html += `</div>`;
}

// БЫЛО:
if (hasDrillingType) {
html += `<div id="drillingTypeInnerTab" class="inner-tab-content">`;
html += displayDrillingTypeForHeader(headerData.drillingType);
html += `</div>`;
}

// СТАЛО:
if (hasDrillingType) {
html += `<div id="drillingTypeInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.drillingType, drillingTableConfigs.drillingType);
html += `</div>`;
}

// БЫЛО:
if (hasSludge) {
html += `<div id="sludgeInnerTab" class="inner-tab-content">`;
html += displayDrillingSludgeForHeader(headerData.sludge);
html += `</div>`;
}

// СТАЛО:
if (hasSludge) {
html += `<div id="sludgeInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.sludge, drillingTableConfigs.sludge);
html += `</div>`;
}

// БЫЛО:
if (hasConditions) {
html += `<div id="conditionsInnerTab" class="inner-tab-content">`;
html += displayDrillingConditionsForHeader(headerData.conditions);
html += `</div>`;
}

// СТАЛО:
if (hasConditions) {
html += `<div id="conditionsInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.conditions, drillingTableConfigs.conditions);
html += `</div>`;
}

// БЫЛО:
if (hasStart) {
html += `<div id="startInnerTab" class="inner-tab-content">`;
html += displayDrillingStartForHeader(headerData.start);
html += `</div>`;
}

// СТАЛО:
if (hasStart) {
html += `<div id="startInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.start, drillingTableConfigs.start);
html += `</div>`;
}

// БЫЛО:
if (hasEnd) {
html += `<div id="endInnerTab" class="inner-tab-content">`;
html += displayDrillingEndForHeader(headerData.end);
html += `</div>`;
}

// СТАЛО:
if (hasEnd) {
html += `<div id="endInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.end, drillingTableConfigs.end);
html += `</div>`;
}
```

**УДАЛИТЬ СТАРЫЕ ФУНКЦИИ ПОЛНОСТЬЮ:**
- `displayDrillingDiameterForHeader`
- `displayDrillingIntervalsForHeader`
- `displayDrillingCasingForHeader`
- `displayDrillingMethodForHeader`
- `displayDrillingTypeForHeader`
- `displayDrillingSludgeForHeader`
- `displayDrillingConditionsForHeader`
- `displayDrillingStartForHeader`
- `displayDrillingEndForHeader`

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

Добавлено (2025-10-08, 11:21)
---------------------------------------------
**ПОЛНАЯ ФУНКЦИЯ `displayDrillingTableForHeader`:**

```javascript
// УНИВЕРСАЛЬНАЯ ФУНКЦИЯ ДЛЯ ВСЕХ ТАБЛИЦ ПРОХОДКИ
function displayDrillingTableForHeader(data, config) {
if (!data || data.length === 0) {
return '<div class="no-data"><p>Нет данных</p></div>';
}

const { tableId, fileName, columns, extraHtml = '' } = config;

let html = createExportButton(tableId, fileName);

html += `
<div class="table-wrapper" id="${tableId}">
<div class="table-header">
<table class="interval-table">
<thead>
<tr>
${columns.map(col =>
`<th style="width: ${col.width};">${col.title}</th>`
).join('')}
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>
`;

data.forEach(item => {
html += '<tr>';
columns.forEach(col => {
let value = item[col.field];

// Применяем форматтер если есть
if (col.formatter) {
value = col.formatter(value, item);
} else if (typeof value === 'number') {
value = formatNumber(value);
}

html += `<td style="width: ${col.width};" title="${value}">${value || ''}</td>`;
});
html += '</tr>';
});

html += `
</tbody>
</table>
</div>
</div>
${extraHtml}
`;

return html;
}

// КОНФИГУРАЦИЯ ДЛЯ ВСЕХ ТИПОВ ТАБЛИЦ ПРОХОДКИ
const drillingTableConfigs = {
diameter: {
tableId: 'drillingDiameterTable',
fileName: 'Диаметр_бурения',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'diameter', title: 'Диаметр (мм)', width: '100px' },
{ field: 'bitType', title: 'Тип коронки', width: '150px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

intervals: {
tableId: 'drillingIntervalsTable',
fileName: 'Интервалы_бурения',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'coreOutput', title: 'Выход керна, %', width: '100px', formatter: formatNumber },
{ field: 'coreDiameter', title: 'Диаметр керна, мм', width: '100px', formatter: formatNumber },
{ field: 'coreLength', title: 'Длина керна, м', width: '100px', formatter: formatNumber },
{ field: 'description', title: 'Описание', width: '200px', formatter: (val) => `<div class="description-text">${val}</div>` },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

casing: {
tableId: 'drillingCasingTable',
fileName: 'Обсадка',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'pipeType', title: 'Тип трубы', width: '120px' },
{ field: 'diameter', title: 'Диаметр (мм)', width: '100px' },
{ field: 'extraction', title: 'Извлечение', width: '100px', formatter: (val) => val === 'Д' ? 'Да' : val === 'Н' ? 'Нет' : val },
{ field: 'wallThickness', title: 'Толщина стенки, мм', width: '100px', formatter: formatNumber },
{ field: 'filterType', title: 'Тип фильтра', width: '120px' },
{ field: 'perforation', title: 'Способ перфорации', width: '120px' },
{ field: 'wellness', title: 'Скважность, %', width: '100px', formatter: formatNumber },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

method: {
tableId: 'drillingMethodTable',
fileName: 'Способ_бурения',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'method', title: 'Способ бурения', width: '200px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

drillingType: {
tableId: 'drillingTypeTable',
fileName: 'Вид_бурения',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'drillingType', title: 'Вид бурения', width: '150px' },
{ field: 'rigType', title: 'Тип бурового станка', width: '150px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

sludge: {
tableId: 'drillingSludgeTable',
fileName: 'Способ_выноски_шлама',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'sludgeMethod', title: 'Способ выноски шлама', width: '200px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

conditions: {
tableId: 'drillingConditionsTable',
fileName: 'Условия_бурения',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'conditions', title: 'Условия бурения', width: '200px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

start: {
tableId: 'drillingStartTable',
fileName: 'Забурка',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'diameter', title: 'Диаметр (мм)', width: '100px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
},

end: {
tableId: 'drillingEndTable',
fileName: 'Разбурка',
columns: [
{ field: 'number', title: 'ПП', width: '50px' },
{ field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber },
{ field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber },
{ field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber },
{ field: 'diameter', title: 'Диаметр (мм)', width: '100px' },
{ field: 'inputDate', title: 'Дата ввода', width: '100px' }
]
}
};

// СПЕЦИАЛЬНАЯ ФУНКЦИЯ ДЛЯ ОБСАДКИ С ДОПОЛНИТЕЛЬНОЙ ИНФОРМАЦИЕЙ
function displayDrillingCasingWithHoles(casingData) {
const config = drillingTableConfigs.casing;
const casingWithHoles = casingData.filter(c => c.holeLength > 0 || c.holeWidth > 0);

let extraHtml = '';
if (casingWithHoles.length > 0) {
extraHtml = `
<div class="info-group" style="margin-top: 20px;">
<div class="info-group-title">Размеры отверстий фильтров</div>
<div class="table-wrapper">
<div class="table-header">
<table class="interval-table">
<thead>
<tr>
<th style="width: 50px;">ПП</th>
<th style="width: 150px;">Интервал</th>
<th style="width: 150px;">Длина отверстия, мм</th>
<th style="width: 150px;">Ширина отверстия, мм</th>
<th style="width: 150px;">Оборудование низа колонны</th>
</tr>
</thead>
</table>
</div>
<div class="table-container">
<table class="interval-table">
<tbody>
`;

casingWithHoles.forEach(casing => {
extraHtml += `
<tr>
<td style="width: 50px;">${casing.number}</td>
<td style="width: 150px;">${formatNumber(casing.from)} - ${formatNumber(casing.to)} м</td>
<td style="width: 150px;">${formatNumber(casing.holeLength)}</td>
<td style="width: 150px;">${formatNumber(casing.holeWidth)}</td>
<td style="width: 150px;">${casing.bottomEquipment}</td>
</tr>
`;
});

extraHtml += `
</tbody>
</table>
</div>
</div>
</div>
`;
}

return displayDrillingTableForHeader(casingData, {
...config,
extraHtml: extraHtml
});
}
```

**ТЕПЕРЬ В `displayDrillingData()` ЗАМЕНЯЕМ ВСЕ ВЫЗОВЫ:**

```javascript
// В функции displayDrillingData() НАЙТИ И ЗАМЕНИТЬ:

// БЫЛО:
if (hasDiameter) {
html += `<div id="diameterInnerTab" class="inner-tab-content">`;
html += displayDrillingDiameterForHeader(headerData.diameter);
html += `</div>`;
}

// СТАЛО:
if (hasDiameter) {
html += `<div id="diameterInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.diameter, drillingTableConfigs.diameter);
html += `</div>`;
}

// БЫЛО:
if (hasIntervals) {
html += `<div id="intervalsInnerTab" class="inner-tab-content">`;
html += displayDrillingIntervalsForHeader(headerData.intervals);
html += `</div>`;
}

// СТАЛО:
if (hasIntervals) {
html += `<div id="intervalsInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.intervals, drillingTableConfigs.intervals);
html += `</div>`;
}

// БЫЛО:
if (hasCasing) {
html += `<div id="casingInnerTab" class="inner-tab-content">`;
html += displayDrillingCasingForHeader(headerData.casing);
html += `</div>`;
}

// СТАЛО:
if (hasCasing) {
html += `<div id="casingInnerTab" class="inner-tab-content">`;
html += displayDrillingCasingWithHoles(headerData.casing);
html += `</div>`;
}

// БЫЛО:
if (hasMethod) {
html += `<div id="methodInnerTab" class="inner-tab-content">`;
html += displayDrillingMethodForHeader(headerData.method);
html += `</div>`;
}

// СТАЛО:
if (hasMethod) {
html += `<div id="methodInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.method, drillingTableConfigs.method);
html += `</div>`;
}

// БЫЛО:
if (hasDrillingType) {
html += `<div id="drillingTypeInnerTab" class="inner-tab-content">`;
html += displayDrillingTypeForHeader(headerData.drillingType);
html += `</div>`;
}

// СТАЛО:
if (hasDrillingType) {
html += `<div id="drillingTypeInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.drillingType, drillingTableConfigs.drillingType);
html += `</div>`;
}

// БЫЛО:
if (hasSludge) {
html += `<div id="sludgeInnerTab" class="inner-tab-content">`;
html += displayDrillingSludgeForHeader(headerData.sludge);
html += `</div>`;
}

// СТАЛО:
if (hasSludge) {
html += `<div id="sludgeInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.sludge, drillingTableConfigs.sludge);
html += `</div>`;
}

// БЫЛО:
if (hasConditions) {
html += `<div id="conditionsInnerTab" class="inner-tab-content">`;
html += displayDrillingConditionsForHeader(headerData.conditions);
html += `</div>`;
}

// СТАЛО:
if (hasConditions) {
html += `<div id="conditionsInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.conditions, drillingTableConfigs.conditions);
html += `</div>`;
}

// БЫЛО:
if (hasStart) {
html += `<div id="startInnerTab" class="inner-tab-content">`;
html += displayDrillingStartForHeader(headerData.start);
html += `</div>`;
}

// СТАЛО:
if (hasStart) {
html += `<div id="startInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.start, drillingTableConfigs.start);
html += `</div>`;
}

// БЫЛО:
if (hasEnd) {
html += `<div id="endInnerTab" class="inner-tab-content">`;
html += displayDrillingEndForHeader(headerData.end);
html += `</div>`;
}

// СТАЛО:
if (hasEnd) {
html += `<div id="endInnerTab" class="inner-tab-content">`;
html += displayDrillingTableForHeader(headerData.end, drillingTableConfigs.end);
html += `</div>`;
}
```

**УДАЛИТЬ СТАРЫЕ ФУНКЦИИ ПОЛНОСТЬЮ:**
- `displayDrillingDiameterForHeader`
- `displayDrillingIntervalsForHeader`
- `displayDrillingCasingForHeader`
- `displayDrillingMethodForHeader`
- `displayDrillingTypeForHeader`
- `displayDrillingSludgeForHeader`
- `displayDrillingConditionsForHeader`
- `displayDrillingStartForHeader`
- `displayDrillingEndForHeader`

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

ffff
Прикрепления:
llll
Прикрепления:
0938251.txt (262.6 Kb)
Понимаю проблему. При переключении между основными вкладками сбрасывается состояние внутренних вкладок (например, в разделе "Проходка"). Нужно сохранять состояние активных внутренних вкладок при смене основных вкладок.

Вот решение:

```javascript
// Добавляем в глобальные переменные
let currentActiveTabs = {
drilling: 'header', // активная вкладка в разделе проходки
// можно добавить другие разделы при необходимости
};

// Функция для сохранения состояния вкладки
function saveActiveTab(section, tabId) {
currentActiveTabs[section] = tabId;
}

// Функция для восстановления состояния вкладки
function restoreActiveTab(section) {
return currentActiveTabs[section] || 'header'; // значение по умолчанию
}

// Модифицируем функцию switchInnerTab
function switchInnerTab(tabId) {
// Сохраняем активную вкладку
saveActiveTab('drilling', tabId);

// Существующий код переключения вкладок
const innerTabs = document.querySelectorAll('.inner-tab');
const innerTabContents = document.querySelectorAll('.inner-tab-content');

innerTabs.forEach(tab => tab.classList.remove('active'));
innerTabContents.forEach(content => content.classList.remove('active'));

const activeTab = document.querySelector(`.inner-tab[data-inner-tab="${tabId}"]`);
const activeContent = document.getElementById(`${tabId}InnerTab`);

if (activeTab) activeTab.classList.add('active');
if (activeContent) activeContent.classList.add('active');
}

// Модифицируем функцию displayDrillingData для восстановления состояния
function displayDrillingData() {
const hasHeaders = xmlData.drillingHeaders && xmlData.drillingHeaders.length > 0;
const hasDrillingData = hasHeaders && xmlData.drillingDataByHeader &&
Object.keys(xmlData.drillingDataByHeader).length > 0;

if (!hasDrillingData) {
return '<div class="no-data"><p>Нет данных проходки</p></div>';
}

let html = '<div class="section">';
html += '<h2>Данные проходки</h2>';

if (!currentDrillingHeaderId && xmlData.drillingHeaders.length > 0) {
currentDrillingHeaderId = xmlData.drillingHeaders[0].id;
}

// ... существующий код заголовков ...

if (currentDrillingHeaderId && xmlData.drillingDataByHeader[currentDrillingHeaderId]) {
const selectedHeader = xmlData.drillingHeaders.find(h => h.id === currentDrillingHeaderId);
const headerData = xmlData.drillingDataByHeader[currentDrillingHeaderId];

html += `
<div class="info-group" style="margin-top: 20px;">
<div class="info-group-title">Данные для параметра: ${selectedHeader.purpose}</div>
</div>
`;

const hasDiameter = headerData.diameter && headerData.diameter.length > 0;
const hasIntervals = headerData.intervals && headerData.intervals.length > 0;
const hasCasing = headerData.casing && headerData.casing.length > 0;
const hasMethod = headerData.method && headerData.method.length > 0;
const hasDrillingType = headerData.drillingType && headerData.drillingType.length > 0;
const hasSludge = headerData.sludge && headerData.sludge.length > 0;
const hasConditions = headerData.conditions && headerData.conditions.length > 0;
const hasStart = headerData.start && headerData.start.length > 0;
const hasEnd = headerData.end && headerData.end.length > 0;

// ВОССТАНАВЛИВАЕМ активную вкладку вместо использования фиксированной
let firstActiveSubTab = restoreActiveTab('drilling');

// Проверяем, что восстановленная вкладка существует
const availableTabs = [];
if (hasDiameter) availableTabs.push('diameter');
if (hasIntervals) availableTabs.push('intervals');
if (hasCasing) availableTabs.push('casing');
if (hasMethod) availableTabs.push('method');
if (hasDrillingType) availableTabs.push('drillingType');
if (hasSludge) availableTabs.push('sludge');
if (hasConditions) availableTabs.push('conditions');
if (hasStart) availableTabs.push('start');
if (hasEnd) availableTabs.push('end');

// Если восстановленная вкладка недоступна, используем первую доступную
if (!availableTabs.includes(firstActiveSubTab) && availableTabs.length > 0) {
firstActiveSubTab = availableTabs[0];
}

if (hasDiameter || hasIntervals || hasCasing || hasMethod || hasDrillingType ||
hasSludge || hasConditions || hasStart || hasEnd) {

html += `<div class="inner-tabs-container">`;
html += `<div class="inner-tabs">`;

html += `<div class="inner-tab ${firstActiveSubTab === 'header' ? 'active' : ''}" data-inner-tab="header">Заголовок</div>`;

if (hasDiameter) {
html += `<div class="inner-tab ${firstActiveSubTab === 'diameter' ? 'active' : ''}" data-inner-tab="diameter">Диаметр бурения</div>`;
}
if (hasIntervals) {
html += `<div class="inner-tab ${firstActiveSubTab === 'intervals' ? 'active' : ''}" data-inner-tab="intervals">Интервалы бурения</div>`;
}
if (hasCasing) {
html += `<div class="inner-tab ${firstActiveSubTab === 'casing' ? 'active' : ''}" data-inner-tab="casing">Обсадка</div>`;
}
if (hasMethod) {
html += `<div class="inner-tab ${firstActiveSubTab === 'method' ? 'active' : ''}" data-inner-tab="method">Способ бурения</div>`;
}
if (hasDrillingType) {
html += `<div class="inner-tab ${firstActiveSubTab === 'drillingType' ? 'active' : ''}" data-inner-tab="drillingType">Вид бурения</div>`;
}
if (hasSludge) {
html += `<div class="inner-tab ${firstActiveSubTab === 'sludge' ? 'active' : ''}" data-inner-tab="sludge">Способ выноски шлама</div>`;
}
if (hasConditions) {
html += `<div class="inner-tab ${firstActiveSubTab === 'conditions' ? 'active' : ''}" data-inner-tab="conditions">Условия бурения</div>`;
}
if (hasStart) {
html += `<div class="inner-tab ${firstActiveSubTab === 'start' ? 'active' : ''}" data-inner-tab="start">Забурка</div>`;
}
if (hasEnd) {
html += `<div class="inner-tab ${firstActiveSubTab === 'end' ? 'active' : ''}" data-inner-tab="end">Разбурка</div>`;
}

html += `</div>`;
html += `<div class="inner-tabs-content">`;

html += `<div id="headerInnerTab" class="inner-tab-content ${firstActiveSubTab === 'header' ? 'active' : ''}">`;
html += displayDrillingHeaderForHeader(selectedHeader);
html += `</div>`;

if (hasDiameter) {
html += `<div id="diameterInnerTab" class="inner-tab-content ${firstActiveSubTab === 'diameter' ? 'active' : ''}">`;
html += displayDrillingTableForHeader(headerData.diameter, drillingTableConfigs.diameter);
html += `</div>`;
}
if (hasIntervals) {
html += `<div id="intervalsInnerTab" class="inner-tab-content ${firstActiveSubTab === 'intervals' ? 'active' : ''}">`;
html += displayDrillingTableForHeader(headerData.intervals, drillingTableConfigs.intervals);
html += `</div>`;
}
if (hasCasing) {
html += `<div id="casingInnerTab" class="inner-tab-content ${firstActiveSubTab === 'casing' ? 'active' : ''}">`;
html += displayDrillingTableForHeader(headerData.casing, drillingTableConfigs.casing);
html += `</div>`;
}
if (hasMethod) {
html += `<div id="methodInnerTab" class="inner-tab-content ${firstActiveSubTab === 'method' ? 'active' : ''}">`;
html += displayDrillingTableForHeader(headerData.method, drillingTableConfigs.method);
html += `</div>`;
}
if (hasDrillingType) {
html += `<div id="drillingTypeInnerTab" class="inner-tab-content ${firstActiveSubTab === 'drillingType' ? 'active' : ''}">`;
html += displayDrillingTableForHeader(headerData.drillingType, drillingTableConfigs.drillingType);
html += `</div>`;
}
if (hasSludge) {
html += `<div id="sludgeInnerTab" class="inner-tab-content ${firstActiveSubTab === 'sludge' ? 'active' : ''}">`;
html += displayDrillingTableForHeader(headerData.sludge, drillingTableConfigs.sludge);
html += `</div>`;
}
if (hasConditions) {
html += `<div id="conditionsInnerTab" class="inner-tab-content ${firstActiveSubTab === 'conditions' ? 'active' : ''}">`;
html += displayDrillingTableForHeader(headerData.conditions, drillingTableConfigs.conditions);
html += `</div>`;
}
if (hasStart) {
html += `<div id="startInnerTab" class="inner-tab-content ${firstActiveSubTab === 'start' ? 'active' : ''}">`;
html += displayDrillingTableForHeader(headerData.start, drillingTableConfigs.start);
html += `</div>`;
}
if (hasEnd) {
html += `<div id="endInnerTab" class="inner-tab-content ${firstActiveSubTab === 'end' ? 'active' : ''}">`;
html += displayDrillingTableForHeader(headerData.end, drillingTableConfigs.end);
html += `</div>`;
}

html += `</div>`;
html += `</div>`;
} else {
html += displayDrillingHeaderForHeader(selectedHeader);
}
} else {
html += '<div class="no-data"><p>Нет данных для выбранного параметра проходки</p></div>';
}

html += '</div>';
return html;
}
```

Также нужно обновить обработчик клика для внутренних вкладок в универсальном обработчике событий:

```javascript
// В универсальном обработчике событий обновляем обработку внутренних вкладок
document.addEventListener('click', function(e) {
// ... существующий код ...

// Обработка внутренних вкладок
if (e.target.classList.contains('inner-tab')) {
const tabId = e.target.getAttribute('data-inner-tab');
switchInnerTab(tabId);
return;
}

// ... остальной код ...
});
```

**Что это решает:**
1. Сохраняет состояние активной внутренней вкладки при переключении между основными вкладками
2. Восстанавливает последнюю активную вкладку при возврате к разделу "Проходка"
3. Обеспечивает плавный пользовательский опыт без потери контекста

Теперь при переключении между вкладками "Первичное документирование", "Итоговое документирование", "Проходка" и другими, состояние внутренних вкладок в разделе "Проходка" будет сохраняться.
Поиск:
Новый ответ
Имя:
Текст сообщения: