From eda4af55fee5a92d98390371b17048d0f82ac857 Mon Sep 17 00:00:00 2001 From: Junya Ishihara Date: Thu, 8 Oct 2026 13:12:24 +0900 Subject: [PATCH] =?UTF-8?q?=E7=AE=A1=E7=90=86=E8=80=85=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=81=8B=E3=82=89=E7=89=B9=E5=88=A5=E3=82=B7=E3=83=95=E3=83=88?= =?UTF-8?q?=E7=94=B3=E8=AB=8B=E3=82=92=E5=89=8A=E9=99=A4=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=81=AA=E3=81=84=E4=B8=8D=E5=85=B7=E5=90=88=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 管理者のシフト一覧カレンダー・全シフト一覧テーブル・シフト詳細モーダルで 特別シフト申請を削除しても、実際には削除されないまま「削除しました」と 表示されていた。特別シフト申請の UUID を通常シフト用の削除 API (deleteMultipleShifts / deleteShift)に送っていたため、shifts テーブルに 該当行がなく 0 件削除で成功扱いになっていた。 チェックボックスや削除ボタンに data-type(special / regular)を持たせ、 特別シフト申請は cancelSpecialShiftApplication でキャンセルするようにした。 UUID の振り分けと種類別の削除は、自分のシフト一覧にあった処理を collectUuidsByType() / deleteShiftsByType() として shifts.js に切り出し、 各画面で共有している。全シフト一覧テーブルのチェックボックスの属性は data-shift-uuid から data-uuids に変更した。 テストは実コードを直接 import して検証している (docs/refactoring/phase-1-test-foundation.md の方針)。 テスト: 430 passed (33 suites) Co-Authored-By: Claude Opus 5.5 --- js/modules/allShiftsTable.js | 14 ++-- js/modules/calendar.js | 9 +-- js/modules/shifts.js | 67 +++++++++++-------- test/admin-special-shift-delete.test.js | 85 +++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 38 deletions(-) create mode 100644 test/admin-special-shift-delete.test.js diff --git a/js/modules/allShiftsTable.js b/js/modules/allShiftsTable.js index 7a239c5..56ccef7 100644 --- a/js/modules/allShiftsTable.js +++ b/js/modules/allShiftsTable.js @@ -196,7 +196,7 @@ function displayAllShiftsTable(shifts, currentPage = 1, itemsPerPage = 50) { ${pageShifts.map(shift => ` - + ${escapeHtml(shift.user_name)} ${formatDateWithWeekday(shift.date)} ${shift.is_special ? '特別' : ''}${escapeHtml(shift.time_slot)}${shift.is_special && shift.shift_name ? `
${escapeHtml(shift.shift_name)}
` : ''} @@ -205,7 +205,7 @@ function displayAllShiftsTable(shifts, currentPage = 1, itemsPerPage = 50) { ${shift.calendar_event_id ? '✓' : '-'} - @@ -275,7 +275,8 @@ function setupAllShiftsCheckboxListeners() { const checkedBoxes = document.querySelectorAll('.shift-row-checkbox:checked'); if (checkedBoxes.length === 0) return; - const uuids = Array.from(checkedBoxes).map(cb => cb.getAttribute('data-shift-uuid')); + const { regularUuids, specialUuids } = collectUuidsByType(checkedBoxes); + const uuids = [...regularUuids, ...specialUuids]; const confirmMessage = `選択した ${uuids.length} 件のシフトを削除しますか?\n\nこの操作は取り消せません。`; if (!confirm(confirmMessage)) return; @@ -284,7 +285,7 @@ function setupAllShiftsCheckboxListeners() { bulkDeleteBtn.textContent = '削除中...'; try { - const result = await API.deleteMultipleShifts(uuids); + const result = await deleteShiftsByType(regularUuids, specialUuids); if (result.success) { alert(`${uuids.length}件のシフトを削除しました。`); loadAllShiftsTable(); @@ -375,7 +376,10 @@ async function handleDeleteShiftFromTable(event) { button.textContent = '削除中...'; try { - const result = await API.deleteShift(shiftUuid); + // 特別シフト申請は専用 API でキャンセルする + const result = button.getAttribute('data-type') === 'special' + ? await API.cancelSpecialShiftApplication(shiftUuid) + : await API.deleteShift(shiftUuid); if (result.success) { alert('シフトを削除しました'); diff --git a/js/modules/calendar.js b/js/modules/calendar.js index 8cf5c17..71efe58 100644 --- a/js/modules/calendar.js +++ b/js/modules/calendar.js @@ -644,6 +644,7 @@ function displayShiftsForDate(container, dateKey) { checkbox.type = 'checkbox'; checkbox.className = 'calendar-shift-checkbox'; checkbox.setAttribute('data-uuids', (shift.uuids || []).join(',')); + checkbox.setAttribute('data-type', shift.isSpecial ? 'special' : 'regular'); checkbox.addEventListener('click', (e) => e.stopPropagation()); checkbox.addEventListener('change', () => { personDiv.classList.toggle('is-selected', checkbox.checked); @@ -703,11 +704,7 @@ function setupCalendarBulkDelete() { const checkedBoxes = document.querySelectorAll('.calendar-shift-checkbox:checked'); if (checkedBoxes.length === 0) return; - const allUuids = []; - checkedBoxes.forEach(cb => { - const uuidsStr = cb.getAttribute('data-uuids'); - if (uuidsStr) uuidsStr.split(',').forEach(uuid => { if (uuid) allUuids.push(uuid); }); - }); + const { regularUuids, specialUuids } = collectUuidsByType(checkedBoxes); if (!confirm(`選択した ${checkedBoxes.length} 件のシフトを削除しますか?\n\nこの操作は取り消せません。`)) return; @@ -715,7 +712,7 @@ function setupCalendarBulkDelete() { bulkDeleteBtn.textContent = '削除中...'; try { - const result = await API.deleteMultipleShifts(allUuids); + const result = await deleteShiftsByType(regularUuids, specialUuids); if (result.success) { alert(`${checkedBoxes.length}件のシフトを削除しました。`); bulkDeleteBtn.textContent = '選択したシフトを削除'; diff --git a/js/modules/shifts.js b/js/modules/shifts.js index 138247a..4305db4 100644 --- a/js/modules/shifts.js +++ b/js/modules/shifts.js @@ -513,6 +513,38 @@ function displayMyShifts(container, shiftsData) { } } +// チェックボックス群から UUID を通常シフト / 特別シフト申請に振り分ける +// (data-uuids: カンマ区切りの UUID、data-type: 'special' なら特別シフト申請) +function collectUuidsByType(checkboxes) { + const regularUuids = []; + const specialUuids = []; + checkboxes.forEach(cb => { + const uuidsStr = cb.getAttribute('data-uuids'); + if (!uuidsStr) return; + const target = cb.getAttribute('data-type') === 'special' ? specialUuids : regularUuids; + uuidsStr.split(',').forEach(uuid => { + if (uuid) target.push(uuid); + }); + }); + return { regularUuids, specialUuids }; +} + +// 通常シフトと特別シフト申請を、それぞれの API で削除する +// (特別シフト申請は shifts テーブルにないため、通常シフト用 API では削除できない) +async function deleteShiftsByType(regularUuids, specialUuids) { + const promises = []; + if (regularUuids.length > 0) { + promises.push(API.deleteMultipleShifts(regularUuids)); + } + specialUuids.forEach(uuid => { + promises.push(API.cancelSpecialShiftApplication(uuid)); + }); + + const results = await Promise.all(promises); + const failed = results.find(r => !r.success); + return failed ? { success: false, error: failed.error } : { success: true }; +} + // 自分のシフト一覧のチェックボックスイベントリスナーをセットアップ function setupMyShiftsCheckboxListeners() { const selectAll = document.getElementById('myShiftsSelectAll'); @@ -545,19 +577,7 @@ function setupMyShiftsCheckboxListeners() { const checkedBoxes = document.querySelectorAll('.my-shift-row-checkbox:checked'); if (checkedBoxes.length === 0) return; - const regularUuids = []; - const specialUuids = []; - checkedBoxes.forEach(cb => { - const uuidsStr = cb.getAttribute('data-uuids'); - const type = cb.getAttribute('data-type'); - if (uuidsStr) { - uuidsStr.split(',').forEach(uuid => { - if (!uuid) return; - if (type === 'special') specialUuids.push(uuid); - else regularUuids.push(uuid); - }); - } - }); + const { regularUuids, specialUuids } = collectUuidsByType(checkedBoxes); if (regularUuids.length === 0 && specialUuids.length === 0) return; @@ -568,18 +588,9 @@ function setupMyShiftsCheckboxListeners() { bulkDeleteBtn.textContent = '削除中...'; try { - const promises = []; - if (regularUuids.length > 0) { - promises.push(API.deleteMultipleShifts(regularUuids)); - } - specialUuids.forEach(uuid => { - promises.push(API.cancelSpecialShiftApplication(uuid)); - }); - - const results = await Promise.all(promises); - const anyFailed = results.some(r => !r.success); + const result = await deleteShiftsByType(regularUuids, specialUuids); - if (!anyFailed) { + if (result.success) { alert(`${checkedBoxes.length}件のシフトを削除しました。`); await loadMyShifts(); } else { @@ -706,8 +717,10 @@ async function deleteShiftFromModal(buttonElement, uuids) { buttonElement.style.opacity = '0.6'; try { - // 複数シフトを一括削除 - const result = await API.deleteMultipleShifts(uuids); + // 複数シフトを一括削除(特別シフト申請は専用 API でキャンセル) + const regularUuids = targetShifts.filter(s => !s.isSpecial).map(s => s.uuid); + const specialUuids = targetShifts.filter(s => s.isSpecial).map(s => s.uuid); + const result = await deleteShiftsByType(regularUuids, specialUuids); if (!result.success) { throw new Error(result.error || 'シフトの削除に失敗しました'); @@ -864,5 +877,5 @@ async function deleteMyShift(buttonElement, uuids, isSpecial) { // (ブラウザでは module が未定義のため no-op になる) // 方針: docs/refactoring/phase-1-test-foundation.md if (typeof module !== 'undefined' && module.exports) { - module.exports = { toAbsoluteApiUrl }; + module.exports = { toAbsoluteApiUrl, collectUuidsByType, deleteShiftsByType }; } diff --git a/test/admin-special-shift-delete.test.js b/test/admin-special-shift-delete.test.js new file mode 100644 index 0000000..665f721 --- /dev/null +++ b/test/admin-special-shift-delete.test.js @@ -0,0 +1,85 @@ +/** + * 管理者画面からの特別シフト申請の削除テスト + * + * 回帰防止: 管理者のシフト一覧カレンダーで特別シフトにチェックを入れて + * 「選択したシフトを削除」しても削除されなかった。 + * 特別シフト申請の UUID を通常シフト用の削除 API + * (POST /api/shifts/delete-multiple) に送っていたため、 + * 0 件削除のまま「削除しました」と表示されていた。 + * + * このテストは実コード(js/modules/shifts.js)を直接 import して検証する。 + * 方針: docs/refactoring/phase-1-test-foundation.md + */ + +const { collectUuidsByType, deleteShiftsByType } = require('../js/modules/shifts.js'); + +function checkbox(uuids, type) { + const attrs = { 'data-uuids': uuids, 'data-type': type }; + return { getAttribute: name => (name in attrs ? attrs[name] : null) }; +} + +describe('collectUuidsByType', () => { + test('特別シフトと通常シフトの UUID を振り分ける', () => { + const result = collectUuidsByType([ + checkbox('r1,r2', 'regular'), + checkbox('s1,s2', 'special'), + checkbox('r3', 'regular') + ]); + + expect(result.regularUuids).toEqual(['r1', 'r2', 'r3']); + expect(result.specialUuids).toEqual(['s1', 's2']); + }); + + test('data-type がない場合は通常シフトとして扱う', () => { + const result = collectUuidsByType([checkbox('r1', null)]); + + expect(result.regularUuids).toEqual(['r1']); + expect(result.specialUuids).toEqual([]); + }); + + test('空の UUID は無視する', () => { + const result = collectUuidsByType([checkbox('', 'special'), checkbox('s1,', 'special')]); + + expect(result.specialUuids).toEqual(['s1']); + }); +}); + +describe('deleteShiftsByType', () => { + beforeEach(() => { + global.API = { + deleteMultipleShifts: jest.fn().mockResolvedValue({ success: true }), + cancelSpecialShiftApplication: jest.fn().mockResolvedValue({ success: true }) + }; + }); + + afterEach(() => { + delete global.API; + }); + + test('特別シフト申請は特別シフト用の API でキャンセルする', async () => { + const result = await deleteShiftsByType([], ['s1', 's2']); + + expect(API.cancelSpecialShiftApplication).toHaveBeenCalledTimes(2); + expect(API.cancelSpecialShiftApplication).toHaveBeenCalledWith('s1'); + expect(API.cancelSpecialShiftApplication).toHaveBeenCalledWith('s2'); + expect(API.deleteMultipleShifts).not.toHaveBeenCalled(); + expect(result.success).toBe(true); + }); + + test('通常シフトと特別シフトが混在していても、それぞれの API に送る', async () => { + const result = await deleteShiftsByType(['r1', 'r2'], ['s1']); + + expect(API.deleteMultipleShifts).toHaveBeenCalledWith(['r1', 'r2']); + expect(API.cancelSpecialShiftApplication).toHaveBeenCalledWith('s1'); + expect(result.success).toBe(true); + }); + + test('いずれかが失敗したら失敗として返す', async () => { + API.cancelSpecialShiftApplication.mockResolvedValue({ success: false, error: '申請が見つかりません' }); + + const result = await deleteShiftsByType(['r1'], ['s1']); + + expect(result.success).toBe(false); + expect(result.error).toBe('申請が見つかりません'); + }); +});