diff --git a/DataCollection.js.html b/DataCollection.js.html new file mode 100644 index 0000000..9c0e85b --- /dev/null +++ b/DataCollection.js.html @@ -0,0 +1,231 @@ + diff --git a/Index.html b/Index.html index 52ed464..c9a2cbd 100644 --- a/Index.html +++ b/Index.html @@ -475,6 +475,7 @@

PDF 下載選項 + diff --git a/JavaScript.html b/JavaScript.html index 27fc5de..b2067f5 100644 --- a/JavaScript.html +++ b/JavaScript.html @@ -687,55 +687,7 @@ } }, - findNextUpcomingClasses: function () { - this.nextUpcomingClassIds.clear(); - - const today = new Date(); - const todayIndex = (today.getDay() === 0) ? 6 : today.getDay() - 1; - - // This feature is only active when viewing today in day mode. - if (this.currentViewMode !== AppConfig.MODES.DAY || this.currentDayIndex !== todayIndex) { - return; - } - - const nowInMinutes = today.getHours() * 60 + today.getMinutes(); - const thresholdInMinutes = 30; - let futureClasses = []; - - // First, find all classes in the future for today - for (const classroom in this.scheduleData) { - if (this.scheduleData[classroom]?.[todayIndex]) { - this.scheduleData[classroom][todayIndex].forEach(course => { - const startTimeInMinutes = this.timeToMinutes(course.timeStart); - if (startTimeInMinutes >= nowInMinutes) { - futureClasses.push(course); - } - }); - } - } - - if (futureClasses.length > 0) { - // Rule 1: Prioritize classes within the 30-minute threshold - const withinThresholdClasses = futureClasses.filter(course => { - const startTimeInMinutes = this.timeToMinutes(course.timeStart); - const timeDifference = startTimeInMinutes - nowInMinutes; - return timeDifference >= 0 && timeDifference <= thresholdInMinutes; - }); - - if (withinThresholdClasses.length > 0) { - withinThresholdClasses.forEach(course => this.nextUpcomingClassIds.add(course.id)); - } else { - // Rule 2: If none in threshold, find the very next one(s) - const nextStartTime = Math.min(...futureClasses.map(c => this.timeToMinutes(c.timeStart))); - futureClasses.forEach(course => { - if (this.timeToMinutes(course.timeStart) === nextStartTime) { - this.nextUpcomingClassIds.add(course.id); - } - }); - } - } - // This function no longer needs to return a value; it just updates the state. - }, + // findNextUpcomingClasses — moved to DataCollection.js.html (Phase 1 PR3) // --- UTILITY --- @@ -746,66 +698,8 @@ // releaseCurrentLock — moved to LockManager.js.html (Phase 1 PR2) // refreshLockHeartbeat — moved to LockManager.js.html (Phase 1 PR2) - _collectFromScheduleData: function (dataSource, collectorFn) { - const resultSet = new Set(); - this._forEachCourse(dataSource, (course) => { - collectorFn(course, resultSet); - }); - return Array.from(resultSet).sort(); - }, - - getAllTags: function () { - return this._collectFromScheduleData(this.scheduleData, (classItem, resultSet) => { - if (classItem.tags && Array.isArray(classItem.tags)) { - classItem.tags.forEach(tag => resultSet.add(tag)); - } - }); - }, - - _collectFromAllCourses: function (collectorFn) { - const resultSet = new Set(); - if (!this.schedules) return []; - - Object.values(this.schedules).forEach(schedule => { - if (schedule.data && schedule.data.scheduleData) { - Object.values(schedule.data.scheduleData).forEach(classroom => { - if (!classroom) return; - Object.values(classroom).forEach(daySchedule => { - if (Array.isArray(daySchedule)) { - daySchedule.forEach(classItem => { - collectorFn(classItem, resultSet); - }); - } - }); - }); - } - }); - return Array.from(resultSet).sort(); - }, - - getGlobalAllTags: function () { - return this._collectFromAllCourses((classItem, resultSet) => { - if (classItem.tags && Array.isArray(classItem.tags)) { - classItem.tags.forEach(tag => resultSet.add(tag)); - } - }); - }, - - getGlobalAllCourseNames: function () { - return this._collectFromAllCourses((classItem, resultSet) => { - if (classItem.name) { - resultSet.add(classItem.name); - } - }); - }, - - getGlobalAllTeachers: function () { - return this._collectFromAllCourses((classItem, resultSet) => { - if (classItem.teacher) { - resultSet.add(classItem.teacher); - } - }); - }, + // _collectFromScheduleData, getAllTags, _collectFromAllCourses — moved to DataCollection.js.html (Phase 1 PR3) + // getGlobalAllTags, getGlobalAllCourseNames, getGlobalAllTeachers — moved to DataCollection.js.html (Phase 1 PR3) // getShortUserName — moved to UtilityFunctions.js.html (Phase 1 PR1) @@ -827,77 +721,12 @@ return this.currentUserEmail === this.getShortUserName(schedule.createdBy); }, - ensureDataIds: function (scheduleData) { - if (!scheduleData) return {}; - Object.values(scheduleData).forEach(classroom => { - if (!classroom) return; - Object.values(classroom).forEach(daySchedule => { - if (Array.isArray(daySchedule)) { - daySchedule.forEach(classItem => { - if (classItem && !classItem.id) { - classItem.id = this.generateUniqueId(); - } - }); - } - }); - }); - return scheduleData; - }, + // ensureDataIds, buildCourseColorMap, sortClassrooms, checkTimeConflict — moved to DataCollection.js.html (Phase 1 PR3) // generateUniqueId — moved to UtilityFunctions.js.html (Phase 1 PR1) // stringToHashCode — moved to UtilityFunctions.js.html (Phase 1 PR1) - buildCourseColorMap: function (dataSource = this.scheduleData) { - this.courseColorMap = {}; - const allNames = new Set(); - if (!dataSource) return; - - Object.values(dataSource).forEach(classroom => { - if (!classroom) return; - Object.values(classroom).forEach(daySchedule => { - if (Array.isArray(daySchedule)) { - daySchedule.forEach(item => allNames.add(item.name)); - } - }); - }); - - const sortedNames = Array.from(allNames).sort(); - sortedNames.forEach(name => { - const hash = this.stringToHashCode(name); - const colorIndex = Math.abs(hash) % AppConfig.COURSE_COLORS.length; - this.courseColorMap[name] = AppConfig.COURSE_COLORS[colorIndex]; - }); - }, - - sortClassrooms: function (classroomList) { - try { - return [...classroomList].sort((a, b) => { - const numA = parseInt((a.match(/\d+/) || ['0'])[0]); - const numB = parseInt((b.match(/\d+/) || ['0'])[0]); - const hundredsA = Math.floor(numA / 100); - const hundredsB = Math.floor(numB / 100); - if (hundredsA !== hundredsB) return hundredsB - hundredsA; - return (numA % 100) - (numB % 100); - }); - } catch (e) { - console.error('排序教室失敗:', e); - return classroomList || []; - } - }, - - checkTimeConflict: function (classroom, day, newClass) { - if (!this.scheduleData[classroom]?.[day]) return false; - const newStart = this.timeToMinutes(newClass.timeStart); - const newEnd = this.timeToMinutes(newClass.timeEnd); - return this.scheduleData[classroom][day].some((existingClass) => { - if (existingClass.id === newClass.id) return false; - const existingStart = this.timeToMinutes(existingClass.timeStart); - const existingEnd = this.timeToMinutes(existingClass.timeEnd); - return newStart < existingEnd && newEnd > existingStart; - }); - }, - // timeToMinutes — moved to UtilityFunctions.js.html (Phase 1 PR1) // formatTime — moved to UtilityFunctions.js.html (Phase 1 PR1) @@ -951,34 +780,7 @@ }); }, - _forEachCourse: function (dataSource, callback) { - if (!dataSource) return; - for (const classroom in dataSource) { - for (const day in dataSource[classroom]) { - dataSource[classroom][day].forEach(course => { - callback(course, classroom, day); - }); - } - } - }, - - countOccurrences: function (predicate) { - let count = 0; - this._forEachCourse(this.scheduleData, course => { - if (predicate(course)) { - count++; - } - }); - return count; - }, - - updateAllOccurrences: function (predicate, updateFn) { - this._forEachCourse(this.scheduleData, course => { - if (predicate(course)) { - updateFn(course); - } - }); - }, + // _forEachCourse, countOccurrences, updateAllOccurrences — moved to DataCollection.js.html (Phase 1 PR3) handleDrop: function (evt) { const { from, to, item, newIndex } = evt; diff --git a/tests/unit/appWiringContracts.test.js b/tests/unit/appWiringContracts.test.js index 2a5fd55..f2c09e5 100644 --- a/tests/unit/appWiringContracts.test.js +++ b/tests/unit/appWiringContracts.test.js @@ -61,15 +61,9 @@ const privateMethods = extractPrivateMethods(jsHtmlSource); */ const EXTRACTED_TO_LIB = new Set([ // stateHelpers.js - 'handleEditClassroom', 'findNextUpcomingClasses', 'saveDataToServer', - 'countOccurrences', 'updateAllOccurrences', - // utilityFunctions.js (remaining in JavaScript.html) - 'sortClassrooms', 'ensureDataIds', - 'buildCourseColorMap', - // dataCollectionHelpers.js - 'getAllTags', 'getGlobalAllTags', 'getGlobalAllCourseNames', 'getGlobalAllTeachers', + 'handleEditClassroom', 'saveDataToServer', // frontendUtils.js - 'checkTimeConflict', 'filterDataByTags', 'filterDataByActiveFilters', + 'filterDataByTags', 'filterDataByActiveFilters', // interactionHelpers.js (handleDrop → applyDrop) 'handleDrop', // appLifecycleHelpers.js (new — this wave) @@ -105,9 +99,6 @@ const NOT_EXTRACTABLE = new Set([ * These are tested indirectly through their public callers. */ const PRIVATE_HELPERS = new Set([ - '_collectFromScheduleData', - '_collectFromAllCourses', - '_forEachCourse', '_filterScheduleData', ]); @@ -123,6 +114,13 @@ const IIFE_EXTRACTED = new Set([ // LockManager.js.html (PR2) '_getLocks', '_saveLocks', 'acquireLock', 'releaseLock', 'releaseCurrentLock', 'refreshLockHeartbeat', + // DataCollection.js.html (PR3) + 'findNextUpcomingClasses', 'countOccurrences', 'updateAllOccurrences', + 'sortClassrooms', 'ensureDataIds', 'buildCourseColorMap', + 'getAllTags', 'getGlobalAllTags', 'getGlobalAllCourseNames', 'getGlobalAllTeachers', + 'checkTimeConflict', + // DataCollection.js.html (PR3) — private helpers also IIFE-extracted + '_forEachCourse', '_collectFromScheduleData', '_collectFromAllCourses', ]); // ─── Tests ───────────────────────────────────────────────────────────────── @@ -135,8 +133,8 @@ describe('JavaScript.html App method wiring contracts (#116)', () => { it('should have expected total App method count', () => { // All public methods (excluding underscore-prefixed private helpers) const publicMethods = appMethods.filter(m => !m.startsWith('_')); - // 48 original - 7 PR1 - 4 PR2 public = 37 remaining in JavaScript.html - expect(publicMethods.length).toBe(37); + // 48 original - 7 PR1 - 4 PR2 public - 11 PR3 public = 26 remaining in JavaScript.html + expect(publicMethods.length).toBe(26); }); it('every public App method should be classified (extracted OR not-extractable)', () => {