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('申請が見つかりません');
+ });
+});