/
Starolat
/
DeepDive
Обзор
Документация
Войти
/
Starolat
/
DeepDive
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
js/services/WeekIndexService.js
361 строка
12 KB
Starolat Sergei
Фаза 6: полное удаление SQL.js и переход на JS Data Engine
28 июн 2026, 10:02
28 июн 2026, 10:02
ff2000f
Код
Авторство
О чём код?
// @ts-check /** * @fileoverview WeekIndexService — Единый источник недельной логики (Вариант В) * @module WeekIndexService * @version 1.0.0 * * Предоставляет: * - Построение ISO-недельного индекса из БД (MIN/MAX дат работ) * - Маппинг date → weekIndex, weekIndex → WeekInfo * - Единый кэш недель per databaseId * * ISO недели: Понедельник 00:00 – Воскресенье 23:59:59.999 * Базовый weekIndex: разница в неделях от 1970-01-05 (первый понедельник Unix epoch). */ import dataIndexer from "./DataIndexer.js"; /** * @typedef {import('../types.js').WeekInfo} WeekInfo * @typedef {import('../types.js').WeekRange} WeekRange */ /** * Базовый понедельник для weekIndex = 0 (1970-01-05T00:00:00Z) * @constant {Date} */ const BASE_MONDAY = new Date("1970-01-05T00:00:00Z"); /** * Миллисекунд в неделе * @constant {number} */ const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000; /** * WeekIndexService — singleton для управления недельными индексами * @class */ class WeekIndexService { constructor() { /** @type {Map<string, WeekInfo[]>} */ this._weeksCache = new Map(); /** @type {Map<string, Map<number, WeekInfo>>} */ this._indexMapCache = new Map(); /** @type {Map<string, { minWeekIndex: number, maxWeekIndex: number, dataDate: string|null }>} */ this._metaCache = new Map(); } // ==================== Core Conversion ==================== /** * Получить понедельник ISO-недели для заданной даты * @param {string|Date} date — ISO-строка или Date * @returns {Date} — понедельник 00:00 UTC */ getMonday(date) { let d; if (typeof date === "string") { d = date.includes("T") ? new Date(date) : new Date(date + "T00:00:00Z"); } else { d = new Date(date); } if (isNaN(d.getTime())) { throw new Error(`Invalid date: ${date}`); } const day = d.getUTCDay(); // 0 = вс, 1 = пн, ... const monday = new Date(d); monday.setUTCDate(d.getUTCDate() - (day === 0 ? 6 : day - 1)); monday.setUTCHours(0, 0, 0, 0); return monday; } /** * Получить воскресенье ISO-недели для заданного понедельника * @param {Date} monday — понедельник недели * @returns {Date} — воскресенье 00:00 UTC */ getSunday(monday) { const sunday = new Date(monday); sunday.setUTCDate(monday.getUTCDate() + 6); sunday.setUTCHours(0, 0, 0, 0); return sunday; } /** * Вычислить weekIndex для даты (разница в неделях от BASE_MONDAY) * @param {string|Date} date * @returns {number} */ getWeekIndex(date) { const monday = this.getMonday(date); return Math.floor((monday.getTime() - BASE_MONDAY.getTime()) / MS_PER_WEEK); } /** * Получить ISO номер недели (1–53) * @param {Date} monday — понедельник недели * @returns {number} */ getISOWeekNumber(monday) { // Копируем, чтобы не мутировать const tmp = new Date(monday); tmp.setUTCDate(tmp.getUTCDate() + 3); // четверг недели const yearStart = new Date(Date.UTC(tmp.getUTCFullYear(), 0, 1)); const dayOfYear = Math.floor( (tmp.getTime() - yearStart.getTime()) / (24 * 60 * 60 * 1000), ) + 1; return Math.ceil(dayOfYear / 7); } /** * Получить Date понедельника по weekIndex * @param {number} weekIndex * @returns {Date} */ getMondayFromIndex(weekIndex) { return new Date(BASE_MONDAY.getTime() + weekIndex * MS_PER_WEEK); } // ==================== Database Cache Building ==================== /** * Построить недельный индекс из базы данных * @param {string} databaseId — ID базы данных * @returns {Promise<WeekInfo[]>} — массив всех недель проекта */ async buildFromDatabase(databaseId) { const indexes = dataIndexer.getIndexes(databaseId); if (!indexes) { throw new Error(`Database ${databaseId} not found in DataIndexer`); } return this.buildFromIndexes(databaseId, indexes); } /** * Построить недельный индекс из JS-индексов (JS Data Engine) * @param {string} databaseId * @param {import('../types.js').DatabaseIndexes} indexes * @returns {WeekInfo[]} */ buildFromIndexes(databaseId, indexes) { const project = Array.from(indexes.projectsById.values())[0]; const dataDate = project?.dataDate || null; const activities = Array.from(indexes.activitiesById.values()); if (activities.length === 0) { this._weeksCache.set(databaseId, []); this._indexMapCache.set(databaseId, new Map()); this._metaCache.set(databaseId, { minWeekIndex: null, maxWeekIndex: null, dataDate: null, }); return []; } let minDate = null; let maxDate = null; for (const a of activities) { if (a.startDate && (!minDate || a.startDate < minDate)) minDate = a.startDate; if (a.endDate && (!maxDate || a.endDate > maxDate)) maxDate = a.endDate; } if (!minDate || !maxDate) { this._weeksCache.set(databaseId, []); this._indexMapCache.set(databaseId, new Map()); this._metaCache.set(databaseId, { minWeekIndex: null, maxWeekIndex: null, dataDate, }); return []; } const dataDateObj = dataDate ? new Date(dataDate.includes("T") ? dataDate : dataDate + "T00:00:00Z") : null; const globalMinMonday = this.getMonday(minDate); const globalMaxSunday = this.getSunday(this.getMonday(maxDate)); /** @type {WeekInfo[]} */ const weeks = []; /** @type {Map<number, WeekInfo>} */ const indexMap = new Map(); let currentMonday = new Date(globalMinMonday); while (currentMonday <= globalMaxSunday) { const weekIndex = Math.floor( (currentMonday.getTime() - BASE_MONDAY.getTime()) / MS_PER_WEEK, ); const currentSunday = this.getSunday(currentMonday); const weekStart = currentMonday.toISOString().split("T")[0]; const weekEnd = currentSunday.toISOString().split("T")[0]; const isPast = dataDateObj ? currentSunday < dataDateObj : false; /** @type {WeekInfo} */ const info = { weekIndex, weekNumber: this.getISOWeekNumber(currentMonday), year: currentSunday.getUTCFullYear(), weekStart, weekEnd, isPast, }; weeks.push(info); indexMap.set(weekIndex, info); currentMonday.setUTCDate(currentMonday.getUTCDate() + 7); } // Обновляем и сами индексы, чтобы потребители могли читать weeks напрямую indexes.weeks = weeks; indexes.minWeekIndex = weeks[0]?.weekIndex ?? 0; indexes.maxWeekIndex = weeks[weeks.length - 1]?.weekIndex ?? 0; this._weeksCache.set(databaseId, weeks); this._indexMapCache.set(databaseId, indexMap); this._metaCache.set(databaseId, { minWeekIndex: indexes.minWeekIndex, maxWeekIndex: indexes.maxWeekIndex, dataDate: dataDate || null, }); console.log( `[WeekIndexService] Built ${weeks.length} weeks for JS DB ${databaseId} (indices ${weeks[0]?.weekIndex}..${weeks[weeks.length - 1]?.weekIndex})`, ); return weeks; } /** * Получить все недели для базы данных * @param {string} databaseId * @returns {WeekInfo[]} */ getAllWeeks(databaseId) { return this._weeksCache.get(databaseId) || []; } /** * Получить WeekInfo по weekIndex * @param {string} databaseId * @param {number} weekIndex * @returns {WeekInfo|null} */ getWeekInfo(databaseId, weekIndex) { const map = this._indexMapCache.get(databaseId); return map?.get(weekIndex) || null; } /** * Получить диапазон недель по индексам (включительно) * @param {string} databaseId * @param {number} startIdx * @param {number} endIdx * @returns {WeekInfo[]} */ getWeekRange(databaseId, startIdx, endIdx) { const weeks = this._weeksCache.get(databaseId) || []; return weeks.filter( (w) => w.weekIndex >= startIdx && w.weekIndex <= endIdx, ); } /** * Найти WeekInfo по дате (в какую неделю попадает дата) * @param {string} databaseId * @param {string|Date} date * @returns {WeekInfo|null} */ findWeekByDate(databaseId, date) { const idx = this.getWeekIndex(date); return this.getWeekInfo(databaseId, idx); } /** * Форматировать метку недели для UI * @param {string} databaseId * @param {number} weekIndex * @param {boolean} [showYear=false] — принудительно показывать год * @returns {string} */ formatWeekLabel(databaseId, weekIndex, showYear = false) { const info = this.getWeekInfo(databaseId, weekIndex); if (!info) return `Нед ${weekIndex}`; const start = info.weekStart.slice(5).replace("-", "."); // MM.DD const end = info.weekEnd.slice(5).replace("-", "."); // MM.DD const yearPart = showYear || info.weekNumber <= 2 ? `, ${info.year}` : ""; return `Нед ${info.weekNumber}${yearPart} · ${start}–${end}`; } /** * Получить мета-информацию диапазона недель БД * @param {string} databaseId * @returns {{ minWeekIndex: number|null, maxWeekIndex: number|null, dataDate: string|null }} */ getMeta(databaseId) { return ( this._metaCache.get(databaseId) || { minWeekIndex: null, maxWeekIndex: null, dataDate: null, } ); } /** * Очистить кэш для базы данных (при закрытии БД) * @param {string} [databaseId] — если не указан, очищает весь кэш */ clearCache(databaseId) { if (databaseId) { this._weeksCache.delete(databaseId); this._indexMapCache.delete(databaseId); this._metaCache.delete(databaseId); console.log(`[WeekIndexService] Cache cleared for DB ${databaseId}`); } else { this._weeksCache.clear(); this._indexMapCache.clear(); this._metaCache.clear(); console.log("[WeekIndexService] All caches cleared"); } } /** * Преобразовать weekEnd (воскресенье) в weekIndex * Legacy helper для миграции от _weekEnd:string к weekIndex * @param {string} weekEnd — дата воскресенья (YYYY-MM-DD) * @returns {number} */ weekEndToIndex(weekEnd) { return this.getWeekIndex(weekEnd); } /** * Преобразовать weekIndex в weekEnd (воскресенье) * @param {number} weekIndex * @returns {string} — YYYY-MM-DD */ indexToWeekEnd(weekIndex) { const monday = this.getMondayFromIndex(weekIndex); const sunday = this.getSunday(monday); return sunday.toISOString().split("T")[0]; } } // Singleton instance const weekIndexService = new WeekIndexService(); // Глобальный доступ для не-модульных скриптов if (typeof window !== "undefined") { window.weekIndexService = weekIndexService; } export { weekIndexService, WeekIndexService }; export default weekIndexService;