diff --git a/js/modules/calendar.js b/js/modules/calendar.js index 71efe58..e799298 100644 --- a/js/modules/calendar.js +++ b/js/modules/calendar.js @@ -530,7 +530,8 @@ function mergeShiftsByPerson(shiftsForDate) { // 個人ごとにグループ化(時間帯とUUIDのマッピングを保持) const shiftsByPerson = {}; shiftsForDate.forEach(shift => { - const personKey = `${getShiftDisplayName(shift)}_${shift.userEmail || shift.email}_${shift.isSpecial ? 'special' : 'regular'}`; + // 表示名が同じ別ユーザーを混ぜないよう、userId で区別する + const personKey = `${getShiftDisplayName(shift)}_${shift.userId}_${shift.isSpecial ? 'special' : 'regular'}`; if (!shiftsByPerson[personKey]) { shiftsByPerson[personKey] = { person: shift, @@ -730,3 +731,10 @@ function setupCalendarBulkDelete() { } }); } + +// テストから実コードを検証できるようにするエクスポートガード +// (ブラウザでは module が未定義のため no-op になる) +// 方針: docs/refactoring/phase-1-test-foundation.md +if (typeof module !== 'undefined' && module.exports) { + module.exports = { mergeShiftsByPerson }; +} diff --git a/js/modules/shiftRequest.js b/js/modules/shiftRequest.js index ad10c14..4420f02 100644 --- a/js/modules/shiftRequest.js +++ b/js/modules/shiftRequest.js @@ -635,6 +635,7 @@ function displayShiftsOnRequestCalendar(allShifts, allSpecialApps) { const regularShifts = (allShifts || []).map(shift => ({ shiftDate: shift.date, timeSlot: shift.time_slot, + userId: shift.user_id, userName: shift.user_name, nickname: shift.nickname, realName: shift.real_name, @@ -645,6 +646,7 @@ function displayShiftsOnRequestCalendar(allShifts, allSpecialApps) { const specialShifts = (allSpecialApps || []).map(app => ({ shiftDate: app.date, timeSlot: app.time_slot, + userId: app.user_id, userName: app.user_name, nickname: app.nickname, realName: app.real_name, diff --git a/js/modules/utils.js b/js/modules/utils.js index 1a92834..9a26ddd 100644 --- a/js/modules/utils.js +++ b/js/modules/utils.js @@ -222,56 +222,6 @@ function getDefaultCapacity(dayOfWeek) { } } -// 管理者用:個人ごとに連続する時間帯をマージする関数 -function mergeShiftsByPerson(shiftsForDate) { - // 個人ごとにグループ化(時間帯とUUIDのマッピングを保持) - const shiftsByPerson = {}; - shiftsForDate.forEach(shift => { - const personKey = `${getShiftDisplayName(shift)}_${shift.userEmail || shift.email}`; - if (!shiftsByPerson[personKey]) { - shiftsByPerson[personKey] = { - person: shift, - shiftsData: [] // 元のシフトデータを保持 - }; - } - shiftsByPerson[personKey].shiftsData.push({ - timeSlot: shift.timeSlot || shift.time, - uuid: shift.uuid - }); - }); - - // 各個人の時間帯をマージ - const mergedShifts = []; - Object.keys(shiftsByPerson).forEach(personKey => { - const personData = shiftsByPerson[personKey]; - - // 時間帯だけを抽出してマージ - const timeSlots = personData.shiftsData.map(s => s.timeSlot); - const mergedTimeSlots = mergeConsecutiveTimeSlots(timeSlots); - - mergedTimeSlots.forEach(mergedTimeSlot => { - // このマージされた時間帯に対応するUUIDだけを抽出 - const correspondingUuids = personData.shiftsData - .filter(s => { - // マージされた時間帯に含まれる元の時間帯かチェック - // 例: 13:00-14:00には13:00-13:30と13:30-14:00が含まれる - const [mergedStart, mergedEnd] = mergedTimeSlot.split('-'); - const [slotStart, slotEnd] = s.timeSlot.split('-'); - return slotStart >= mergedStart && slotEnd <= mergedEnd; - }) - .map(s => s.uuid); - - mergedShifts.push({ - ...personData.person, - timeSlot: mergedTimeSlot, - uuids: correspondingUuids // このマージ時間帯に対応するUUIDのみ - }); - }); - }); - - return mergedShifts; -} - // 一括削除アクションバーのHTML文字列を生成する関数 function createBulkActionBarHTML(barId, countId, btnId) { return `
@@ -308,3 +258,10 @@ function getCurrentUserData() { return null; } + +// テストから実コードを検証できるようにするエクスポートガード +// (ブラウザでは module が未定義のため no-op になる) +// 方針: docs/refactoring/phase-1-test-foundation.md +if (typeof module !== 'undefined' && module.exports) { + module.exports = { getDisplayName, getShiftDisplayName, mergeConsecutiveTimeSlots }; +} diff --git a/test/calendar-merge-same-display-name.test.js b/test/calendar-merge-same-display-name.test.js new file mode 100644 index 0000000..932279d --- /dev/null +++ b/test/calendar-merge-same-display-name.test.js @@ -0,0 +1,73 @@ +/** + * カレンダーの人物ごとのシフトマージのテスト + * + * 回帰防止: 表示名が同じ別ユーザーが、カレンダー上で 1 人にまとめられていた。 + * 人物を区別するキーに使っていた userEmail / email はシフトデータに + * 含まれておらず、実質的に表示名だけで区別していたため。 + * 同じ時間帯に入っていると、片方のチェックボックスに両方の UUID が入り、 + * 1 人分を削除したつもりでもう 1 人のシフトまで削除されていた。 + * + * このテストは実コード(js/modules/calendar.js)を直接 import して検証する。 + * 方針: docs/refactoring/phase-1-test-foundation.md + */ + +const { getShiftDisplayName, mergeConsecutiveTimeSlots } = require('../js/modules/utils.js'); +const { mergeShiftsByPerson } = require('../js/modules/calendar.js'); + +function shift(userId, userName, timeSlot, uuid, isSpecial = false) { + return { shiftDate: '2026-10-08', userId, userName, timeSlot, uuid, isSpecial }; +} + +describe('mergeShiftsByPerson: 表示名が同じ別ユーザー', () => { + beforeEach(() => { + global.getShiftDisplayName = getShiftDisplayName; + global.mergeConsecutiveTimeSlots = mergeConsecutiveTimeSlots; + }); + + afterEach(() => { + delete global.getShiftDisplayName; + delete global.mergeConsecutiveTimeSlots; + }); + + test('同じ時間帯に入っていても、ユーザーごとに別のエントリになる', () => { + const result = mergeShiftsByPerson([ + shift('user-1', '田中', '13:00-13:30', 'uuid-1'), + shift('user-2', '田中', '13:00-13:30', 'uuid-2') + ]); + + expect(result).toHaveLength(2); + expect(result.find(s => s.userId === 'user-1').uuids).toEqual(['uuid-1']); + expect(result.find(s => s.userId === 'user-2').uuids).toEqual(['uuid-2']); + }); + + test('連続する時間帯でも、別ユーザーのシフトは 1 つにまとめない', () => { + const result = mergeShiftsByPerson([ + shift('user-1', '田中', '13:00-13:30', 'uuid-1'), + shift('user-2', '田中', '13:30-14:00', 'uuid-2') + ]); + + expect(result.map(s => s.timeSlot).sort()).toEqual(['13:00-13:30', '13:30-14:00']); + expect(result.find(s => s.userId === 'user-1').uuids).toEqual(['uuid-1']); + expect(result.find(s => s.userId === 'user-2').uuids).toEqual(['uuid-2']); + }); + + test('同じユーザーの連続する時間帯は、これまでどおり 1 つにまとめる', () => { + const result = mergeShiftsByPerson([ + shift('user-1', '田中', '13:00-13:30', 'uuid-1'), + shift('user-1', '田中', '13:30-14:00', 'uuid-2') + ]); + + expect(result).toHaveLength(1); + expect(result[0].timeSlot).toBe('13:00-14:00'); + expect(result[0].uuids).toEqual(['uuid-1', 'uuid-2']); + }); + + test('同じユーザーの通常シフトと特別シフトは、これまでどおり別のエントリになる', () => { + const result = mergeShiftsByPerson([ + shift('user-1', '田中', '13:00-13:30', 'uuid-r'), + shift('user-1', '田中', '13:30-14:00', 'uuid-s', true) + ]); + + expect(result).toHaveLength(2); + }); +});