From 4e0c0472fc6547f9ebff424e8f3e47a18c0bef96 Mon Sep 17 00:00:00 2001 From: Junya Ishihara Date: Wed, 20 May 2026 15:34:15 +0900 Subject: [PATCH] =?UTF-8?q?=E5=8B=A4=E5=8B=99=E8=A8=98=E9=8C=B2=E3=82=BF?= =?UTF-8?q?=E3=83=96=E3=82=92=E6=9C=AC=E5=90=8D=E8=A1=A8=E7=A4=BA=E3=83=BB?= =?UTF-8?q?=E5=90=8C=E5=90=8D=E3=83=9E=E3=83=BC=E3=82=B8=E5=AF=BE=E5=BF=9C?= =?UTF-8?q?=E3=81=AB=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 通常シフトAPIのgetByDateRangeにusersテーブルのJOINを追加しreal_nameを取得 - 勤務記録の一番左カラムをuser_nameからreal_name(本名)に変更 - 表示名(本名)が同じユーザーのシフトを同一人物としてマージ - プルダウンも本名単位で表示 - 表示月の変更で自動的に再読み込み(ボタン不要) - 同名マージ挙動のテストを追加(5件) Co-Authored-By: Claude Sonnet 4.6 --- js/modules/workRecords.js | 19 ++++++--- server/src/models/Shift.ts | 8 +++- test/work-records.test.js | 87 +++++++++++++++++++++++++++++++++----- 3 files changed, 96 insertions(+), 18 deletions(-) diff --git a/js/modules/workRecords.js b/js/modules/workRecords.js index 1e4f1b5..88bd4c2 100644 --- a/js/modules/workRecords.js +++ b/js/modules/workRecords.js @@ -74,9 +74,10 @@ function buildShiftRows(regularShifts, specialShifts) { const regularGroups = {}; for (const shift of regularShifts) { - const key = `${shift.user_id}::${shift.date}`; + const displayName = shift.real_name || shift.user_name; + const key = `${displayName}::${shift.date}`; if (!regularGroups[key]) { - regularGroups[key] = { user_id: shift.user_id, user_name: shift.user_name, date: shift.date, slots: [] }; + regularGroups[key] = { user_id: displayName, user_name: displayName, date: shift.date, slots: [] }; } regularGroups[key].slots.push(shift.time_slot); } @@ -89,9 +90,10 @@ function buildShiftRows(regularShifts, specialShifts) { const specialGroups = {}; for (const shift of specialShifts) { - const key = `${shift.user_id}::${shift.special_shift_uuid || shift.date}`; + const displayName = shift.real_name || shift.user_name; + const key = `${displayName}::${shift.special_shift_uuid || shift.date}`; if (!specialGroups[key]) { - specialGroups[key] = { user_id: shift.user_id, user_name: shift.user_name, date: shift.date, slots: [] }; + specialGroups[key] = { user_id: displayName, user_name: displayName, date: shift.date, slots: [] }; } if (shift.time_slot) specialGroups[key].slots.push(shift.time_slot); } @@ -203,12 +205,15 @@ function renderWorkRecords(container, rows, year, month) { `; - document.getElementById('workRecordsApplyBtn').addEventListener('click', () => { + const applyMonth = () => { const val = document.getElementById('workRecordsMonth').value; if (!val) return; const [y, m] = val.split('-').map(Number); loadWorkRecords(y, m); - }); + }; + + document.getElementById('workRecordsApplyBtn').addEventListener('click', applyMonth); + document.getElementById('workRecordsMonth').addEventListener('change', applyMonth); document.getElementById('workRecordsUserFilter').addEventListener('change', () => { applyUserFilter(rows); @@ -263,7 +268,7 @@ function buildTableHTML(rows) { - + diff --git a/server/src/models/Shift.ts b/server/src/models/Shift.ts index 07a6d8b..c9b035e 100644 --- a/server/src/models/Shift.ts +++ b/server/src/models/Shift.ts @@ -98,7 +98,13 @@ export class ShiftModel { */ static getByDateRange(startDate: string, endDate: string): Shift[] { try { - const stmt = db.prepare('SELECT * FROM shifts WHERE date >= ? AND date <= ? ORDER BY date ASC, time_slot ASC'); + const stmt = db.prepare(` + SELECT s.*, u.nickname, u.real_name, u.email + FROM shifts s + LEFT JOIN users u ON s.user_id = u.user_id + WHERE s.date >= ? AND s.date <= ? + ORDER BY s.date ASC, s.time_slot ASC + `); return stmt.all(startDate, endDate) as Shift[]; } catch (error) { console.error('Error getting shifts by date range:', error); diff --git a/test/work-records.test.js b/test/work-records.test.js index 149cf7b..0051c4a 100644 --- a/test/work-records.test.js +++ b/test/work-records.test.js @@ -76,12 +76,13 @@ function getDayOfWeek(dateString) { function buildShiftRows(regularShifts, specialShifts) { const rows = []; - // 通常シフト: user_id + date でグループ化し連続スロットをマージ + // 通常シフト: 本名 + date でグループ化(同名ユーザーはマージ) const regularGroups = {}; for (const shift of regularShifts) { - const key = `${shift.user_id}::${shift.date}`; + const displayName = shift.real_name || shift.user_name; + const key = `${displayName}::${shift.date}`; if (!regularGroups[key]) { - regularGroups[key] = { user_id: shift.user_id, user_name: shift.user_name, date: shift.date, slots: [] }; + regularGroups[key] = { user_id: displayName, user_name: displayName, date: shift.date, slots: [] }; } regularGroups[key].slots.push(shift.time_slot); } @@ -92,12 +93,13 @@ function buildShiftRows(regularShifts, specialShifts) { } } - // 特別シフト: user_id + special_shift_uuid でグループ化し連続スロットをマージ + // 特別シフト: 本名 + special_shift_uuid でグループ化(同名ユーザーはマージ) const specialGroups = {}; for (const shift of specialShifts) { - const key = `${shift.user_id}::${shift.special_shift_uuid || shift.date}`; + const displayName = shift.real_name || shift.user_name; + const key = `${displayName}::${shift.special_shift_uuid || shift.date}`; if (!specialGroups[key]) { - specialGroups[key] = { user_id: shift.user_id, user_name: shift.user_name, date: shift.date, slots: [] }; + specialGroups[key] = { user_id: displayName, user_name: displayName, date: shift.date, slots: [] }; } if (shift.time_slot) specialGroups[key].slots.push(shift.time_slot); } @@ -223,7 +225,7 @@ describe('buildShiftRows', () => { test('連続する通常シフトスロットは1行にマージされる', () => { const rows = buildShiftRows(regularShifts, []); - const u1Regular = rows.filter(r => r.user_id === 'u1' && r.type === '通常'); + const u1Regular = rows.filter(r => r.user_id === '山田太郎' && r.type === '通常'); // 14:00-15:30 と 17:00-17:30 の2行 expect(u1Regular).toHaveLength(2); expect(u1Regular[0].start).toBe('14:00'); @@ -232,7 +234,7 @@ describe('buildShiftRows', () => { test('非連続スロットは別行になる', () => { const rows = buildShiftRows(regularShifts, []); - const u1Regular = rows.filter(r => r.user_id === 'u1' && r.type === '通常'); + const u1Regular = rows.filter(r => r.user_id === '山田太郎' && r.type === '通常'); expect(u1Regular[1].start).toBe('17:00'); expect(u1Regular[1].end).toBe('17:30'); }); @@ -243,10 +245,14 @@ describe('buildShiftRows', () => { expect(merged.hours).toBe(1.5); }); + test('user_idは表示名(本名)になる', () => { + const rows = buildShiftRows(regularShifts, []); + expect(rows.every(r => r.user_id === r.user_name)).toBe(true); + }); + test('特別シフトも連続スロットはマージされる', () => { const rows = buildShiftRows([], specialShifts); - const sp1Rows = rows.filter(r => r.user_id === 'u1'); - const sp1Merged = sp1Rows.find(r => r.start === '09:00'); + const sp1Merged = rows.find(r => r.start === '09:00'); expect(sp1Merged).toBeTruthy(); expect(sp1Merged.end).toBe('10:00'); }); @@ -268,6 +274,67 @@ describe('buildShiftRows', () => { }); }); +describe('buildShiftRows - 同名ユーザーのマージ', () => { + test('real_nameが同じ通常シフトは同一人物としてマージされる', () => { + const shifts = [ + { user_id: 'u1', user_name: '旧名A', real_name: '山田太郎', date: '2026-04-21', time_slot: '13:00-13:30' }, + { user_id: 'u2', user_name: '旧名B', real_name: '山田太郎', date: '2026-04-21', time_slot: '13:30-14:00' }, + ]; + const rows = buildShiftRows(shifts, []); + // 同じ日に同じ本名 → 連続スロットとして1行にマージ + expect(rows).toHaveLength(1); + expect(rows[0].user_name).toBe('山田太郎'); + expect(rows[0].start).toBe('13:00'); + expect(rows[0].end).toBe('14:00'); + }); + + test('real_nameが同じでも別の日付は別行になる', () => { + const shifts = [ + { user_id: 'u1', user_name: '旧名A', real_name: '山田太郎', date: '2026-04-21', time_slot: '13:00-13:30' }, + { user_id: 'u2', user_name: '旧名B', real_name: '山田太郎', date: '2026-04-22', time_slot: '13:00-13:30' }, + ]; + const rows = buildShiftRows(shifts, []); + expect(rows).toHaveLength(2); + expect(rows[0].user_name).toBe('山田太郎'); + expect(rows[1].user_name).toBe('山田太郎'); + }); + + test('real_nameが異なるユーザーは別々に表示される', () => { + const shifts = [ + { user_id: 'u1', user_name: '山田太郎', real_name: '山田太郎', date: '2026-04-21', time_slot: '13:00-13:30' }, + { user_id: 'u2', user_name: '佐藤花子', real_name: '佐藤花子', date: '2026-04-21', time_slot: '13:00-13:30' }, + ]; + const rows = buildShiftRows(shifts, []); + expect(rows).toHaveLength(2); + const names = rows.map(r => r.user_name).sort(); + expect(names).toEqual(['佐藤花子', '山田太郎']); + }); + + test('real_nameがない場合はuser_nameにフォールバックしてマージされる', () => { + const shifts = [ + { user_id: 'u1', user_name: '山田太郎', date: '2026-04-21', time_slot: '13:00-13:30' }, + { user_id: 'u2', user_name: '山田太郎', date: '2026-04-21', time_slot: '13:30-14:00' }, + ]; + const rows = buildShiftRows(shifts, []); + expect(rows).toHaveLength(1); + expect(rows[0].user_name).toBe('山田太郎'); + expect(rows[0].start).toBe('13:00'); + expect(rows[0].end).toBe('14:00'); + }); + + test('特別シフトでもreal_nameが同じユーザーは同一special_shift_uuid内でマージされる', () => { + const specials = [ + { user_id: 'u1', user_name: '旧名A', real_name: '山田太郎', date: '2026-04-21', special_shift_uuid: 'sp1', time_slot: '09:00-09:30' }, + { user_id: 'u2', user_name: '旧名B', real_name: '山田太郎', date: '2026-04-21', special_shift_uuid: 'sp1', time_slot: '09:30-10:00' }, + ]; + const rows = buildShiftRows([], specials); + expect(rows).toHaveLength(1); + expect(rows[0].user_name).toBe('山田太郎'); + expect(rows[0].start).toBe('09:00'); + expect(rows[0].end).toBe('10:00'); + }); +}); + describe('buildTsvContent', () => { const rows = [ { user_name: '山田太郎', date: '2026-04-21', start: '14:00', end: '18:00', hours: 4 },
ユーザー名本名 日付 曜日 勤務開始