/** * Модуль вкладки "Срезы": список снапшотов по диапазону дат, создание, инспекция и генерация отчетов. */ // 1. Вспомогательные функции форматирования дат function formatDateDDMMYYYY(d) { const day = String(d.getDate()).padStart(2, '0'); const month = String(d.getMonth() + 1).padStart(2, '0'); const year = d.getFullYear(); return `${day}.${month}.${year}`; } function formatDateToISO(d) { const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function isoToBackendFormat(isoStr) { if (!isoStr) return ''; if (isoStr.includes('.')) return isoStr; const [y, m, d] = isoStr.split('-'); return `${d}.${m}.${y}`; } // 2. Автозаполнение полей создания среза текущими датой и временем function initManualSnapshotInputs() { const now = new Date(); const dateInput = document.getElementById('manual-snapshot-date'); const timeInput = document.getElementById('manual-snapshot-time'); if (dateInput) { dateInput.value = formatDateDDMMYYYY(now); } if (timeInput) { const hh = String(now.getHours()).padStart(2, '0'); const mm = String(now.getMinutes()).padStart(2, '0'); timeInput.value = `${hh}:${mm}`; } } // 3. Быстрые пресеты периода window.setSnapshotDatePreset = function(preset) { const today = new Date(); const fromInput = document.getElementById('snapshots-date-from'); const toInput = document.getElementById('snapshots-date-to'); if (!fromInput || !toInput) return; if (preset === 'today') { const str = formatDateToISO(today); fromInput.value = str; toInput.value = str; } else if (preset === 'yesterday') { const y = new Date(); y.setDate(today.getDate() - 1); const str = formatDateToISO(y); fromInput.value = str; toInput.value = str; } else if (preset === 'days3') { const start = new Date(); start.setDate(today.getDate() - 2); fromInput.value = formatDateToISO(start); toInput.value = formatDateToISO(today); } else if (preset === 'days7') { const start = new Date(); start.setDate(today.getDate() - 6); fromInput.value = formatDateToISO(start); toInput.value = formatDateToISO(today); } loadSnapshotsView(); }; // 4. Загрузка списка срезов с группировкой и сортировкой async function loadSnapshotsView() { // Гарантированно заполняем поля даты и времени создания среза initManualSnapshotInputs(); const listContainer = document.getElementById('snapshots-list'); const countBadge = document.getElementById('snapshots-count-badge'); const fromInput = document.getElementById('snapshots-date-from'); const toInput = document.getElementById('snapshots-date-to'); if (!listContainer) return; const todayIso = formatDateToISO(new Date()); if (fromInput && !fromInput.value) fromInput.value = todayIso; if (toInput && !toInput.value) toInput.value = todayIso; const dateFrom = fromInput ? isoToBackendFormat(fromInput.value) : isoToBackendFormat(todayIso); const dateTo = toInput ? isoToBackendFormat(toInput.value) : dateFrom; listContainer.innerHTML = `
Загрузка срезов...
`; try { const url = `/api/v1/snapshots?date_from=${encodeURIComponent(dateFrom)}&date_to=${encodeURIComponent(dateTo)}`; const res = await fetch(url, { headers: AuthManager.getAuthHeaders() }); if (!res.ok) throw new Error('Ошибка сети'); const data = await res.json(); const snapshots = data.snapshots || []; if (countBadge) countBadge.innerText = `Срезы в базе: ${snapshots.length}`; if (snapshots.length === 0) { listContainer.innerHTML = `

За выбранный период срезы не найдены

`; return; } // Вспомогательный ключ для сортировки от новых к старым function getSortKey(s) { const sid = (s.snapshot_id || s.id || '').toUpperCase(); const digits = sid.replace(/\D/g, ''); if (sid.includes('FINAL') && digits.length >= 8) { return `${digits.substring(0, 8)}_235959`; } return digits.padEnd(14, '0'); } snapshots.sort((a, b) => getSortKey(b).localeCompare(getSortKey(a))); // Группировка по дням const groups = {}; snapshots.forEach(s => { const sid = s.snapshot_id || s.id || ''; let groupDate = s.date || ''; if (!groupDate) { const digits = sid.replace(/\D/g, ''); if (digits.length >= 8) { const y = digits.substring(0, 4); const m = digits.substring(4, 6); const d = digits.substring(6, 8); groupDate = `${d}.${m}.${y}`; } else { groupDate = 'Другие срезы'; } } if (!groups[groupDate]) groups[groupDate] = []; groups[groupDate].push(s); }); // Сортировка дат по убыванию const sortedDates = Object.keys(groups).sort((d1, d2) => { const parseDate = (str) => { const p = str.split('.'); return p.length === 3 ? new Date(p[2], p[1] - 1, p[0]).getTime() : 0; }; return parseDate(d2) - parseDate(d1); }); let html = ''; for (const dateLabel of sortedDates) { const items = groups[dateLabel]; html += `
${escapeHtml(dateLabel)}
${items.length} срез.
`; html += items.map(s => { const sid = s.snapshot_id || s.id; const isFinal = Boolean(s.is_final) || sid.toUpperCase().includes('FINAL'); const badgeFinal = isFinal ? `Финал Y` : ''; const cnt = s.record_count ?? s.count ?? s.records_count ?? 0; const timeStr = s.snapshot_time || s.time || '—'; return `
${escapeHtml(sid)} ${badgeFinal}
${escapeHtml(timeStr)} · ${cnt} зап.
${!isFinal ? ` ` : ''}
`; }).join(''); } listContainer.innerHTML = html; } catch (e) { listContainer.innerHTML = `
Не удалось загрузить срезы
`; } } // 5. Ручное создание среза async function createSnapshotManual() { const dateVal = document.getElementById('manual-snapshot-date')?.value || ''; const timeVal = document.getElementById('manual-snapshot-time')?.value || ''; const btn = document.getElementById('btn-create-snapshot'); if (!dateVal) { alert('Укажите дату среза'); return; } btn.disabled = true; btn.innerHTML = ` Создание...`; try { const res = await fetch('/api/v1/snapshots/create', { method: 'POST', headers: { ...AuthManager.getAuthHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify({ date_str: dateVal, time_str: timeVal }) }); if (res.ok) { loadSnapshotsView(); } else { const data = await res.json(); alert(`Ошибка: ${data.detail || 'Не удалось сформировать срез'}`); } } catch (e) { alert('Ошибка сети'); } finally { btn.disabled = false; btn.innerHTML = ` Сделать срез`; } } // 6. Удаление среза async function deleteSnapshotItem(snapshotId) { if (!confirm(`Удалить срез ${snapshotId}?`)) return; try { const res = await fetch('/api/v1/snapshots', { method: 'DELETE', headers: { ...AuthManager.getAuthHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify({ snapshot_ids: [snapshotId] }) }); if (res.ok) loadSnapshotsView(); } catch (e) { alert('Ошибка сети'); } } // 7. Генерация отчетов On-Demand window.generateReportDirect = async function(reportType) { const fromInput = document.getElementById('snapshots-date-from'); const dateInput = fromInput ? isoToBackendFormat(fromInput.value) : ''; const labelMap = { 'SVODKA': 'Сводки', 'SIMPLIFIED': 'Упрощенного отчета', 'DETAILED': 'Детального отчета' }; const targetLabel = labelMap[reportType] || 'отчета'; try { const res = await fetch('/api/v1/reports/generate', { method: 'POST', headers: { ...AuthManager.getAuthHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify({ report_type: reportType, date: dateInput || null, time: null // ⭐️ Всегда берем последний готовый срез }) }); if (!res.ok) { alert(`Ошибка сервера (${res.status}) при создании ${targetLabel}`); return; } const data = await res.json(); const rep = data.reports?.[0]; if (rep && rep.status === 'success' && rep.download_url) { window.open(rep.download_url, '_blank'); } else { alert(`Ошибка: ${rep?.error || 'Не удалось сформировать файл'}`); } } catch (e) { console.error('[Reports] Ошибка генерации:', e); alert('Сетевая ошибка при запросе формирования отчета'); } }; // 8. Инициализация при первичной загрузке страницы document.addEventListener('DOMContentLoaded', () => { const todayIso = formatDateToISO(new Date()); const fromInput = document.getElementById('snapshots-date-from'); const toInput = document.getElementById('snapshots-date-to'); if (fromInput && !fromInput.value) fromInput.value = todayIso; if (toInput && !toInput.value) toInput.value = todayIso; initManualSnapshotInputs(); }); // Экспортируем в глобальную область, чтобы core.js при переключении вкладок вызывал эту функцию window.loadSnapshotsView = loadSnapshotsView; window.createSnapshotManual = createSnapshotManual; window.deleteSnapshotItem = deleteSnapshotItem;