Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions js/modules/allShiftsTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ function displayAllShiftsTable(shifts, currentPage = 1, itemsPerPage = 50) {
<tbody>
${pageShifts.map(shift => `
<tr data-shift-uuid="${escapeHtml(shift.uuid)}">
<td style="text-align: center;"><input type="checkbox" class="shift-row-checkbox" data-shift-uuid="${escapeHtml(shift.uuid)}" data-shift-info="${escapeHtml(shift.user_name)} / ${escapeHtml(shift.date)} ${escapeHtml(shift.time_slot)}"></td>
<td style="text-align: center;"><input type="checkbox" class="shift-row-checkbox" data-uuids="${escapeHtml(shift.uuid)}" data-type="${shift.is_special ? 'special' : 'regular'}" data-shift-info="${escapeHtml(shift.user_name)} / ${escapeHtml(shift.date)} ${escapeHtml(shift.time_slot)}"></td>
<td>${escapeHtml(shift.user_name)}</td>
<td>${formatDateWithWeekday(shift.date)}</td>
<td><span class="shift-time-inner">${shift.is_special ? '<span class="special-badge">特別</span>' : '<span class="special-badge-placeholder"></span>'}${escapeHtml(shift.time_slot)}</span>${shift.is_special && shift.shift_name ? `<div class="shift-name-label">${escapeHtml(shift.shift_name)}</div>` : ''}</td>
Expand All @@ -205,7 +205,7 @@ function displayAllShiftsTable(shifts, currentPage = 1, itemsPerPage = 50) {
${shift.calendar_event_id ? '<span style="color: #4CAF50;">✓</span>' : '<span style="color: #999;">-</span>'}
</td>
<td>
<button class="delete-shift-table-btn" data-shift-uuid="${escapeHtml(shift.uuid)}" data-shift-info="${escapeHtml(shift.user_name)} / ${escapeHtml(shift.date)} ${escapeHtml(shift.time_slot)}">
<button class="delete-shift-table-btn" data-shift-uuid="${escapeHtml(shift.uuid)}" data-type="${shift.is_special ? 'special' : 'regular'}" data-shift-info="${escapeHtml(shift.user_name)} / ${escapeHtml(shift.date)} ${escapeHtml(shift.time_slot)}">
削除
</button>
</td>
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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('シフトを削除しました');
Expand Down
9 changes: 3 additions & 6 deletions js/modules/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -703,19 +704,15 @@ 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;

bulkDeleteBtn.disabled = true;
bulkDeleteBtn.textContent = '削除中...';

try {
const result = await API.deleteMultipleShifts(allUuids);
const result = await deleteShiftsByType(regularUuids, specialUuids);
if (result.success) {
alert(`${checkedBoxes.length}件のシフトを削除しました。`);
bulkDeleteBtn.textContent = '選択したシフトを削除';
Expand Down
67 changes: 40 additions & 27 deletions js/modules/shifts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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;

Expand All @@ -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 {
Expand Down Expand Up @@ -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 || 'シフトの削除に失敗しました');
Expand Down Expand Up @@ -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 };
}
85 changes: 85 additions & 0 deletions test/admin-special-shift-delete.test.js
Original file line number Diff line number Diff line change
@@ -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('申請が見つかりません');
});
});
Loading