diff --git a/apps/web/app/admin/members/page.tsx b/apps/web/app/admin/members/page.tsx index 6719a71..4977ff0 100644 --- a/apps/web/app/admin/members/page.tsx +++ b/apps/web/app/admin/members/page.tsx @@ -155,49 +155,51 @@ function MembersPageContent() { -
+
{filteredMembers.length === 0 ? (

暂无成员

) : ( -
- - - - - - - - - - - - - {filteredMembers.map((m) => ( - - - - - - - +
+
+
姓名用户名角色部门企业邮箱操作
{m.name}{m.username ?? '-'} - {roleName(m.roleId)} - {departments.find((d) => d.id === m.departmentId)?.name ?? '-'}{m.email} -
- - {!isSystemAdminMember(m) && ( - - )} - {isSystemAdminMember(m) && ( - 系统账号 - )} -
-
+ + + + + + + + - ))} - -
姓名用户名角色部门企业邮箱操作
+ + + {filteredMembers.map((m) => ( + + {m.name} + {m.username ?? '-'} + + {roleName(m.roleId)} + + {departments.find((d) => d.id === m.departmentId)?.name ?? '-'} + {m.email} + +
+ + {!isSystemAdminMember(m) && ( + + )} + {isSystemAdminMember(m) && ( + 系统账号 + )} +
+ + + ))} + + +
)}
diff --git a/apps/web/app/overtime/page.tsx b/apps/web/app/overtime/page.tsx index d27ca89..4e90f7c 100644 --- a/apps/web/app/overtime/page.tsx +++ b/apps/web/app/overtime/page.tsx @@ -8,7 +8,7 @@ import { useRequirementStore } from '@/stores/useRequirementStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { flattenProjects, flattenVersions } from '@/lib/derive'; -import { calcDuration, filterOvertimeRecordsForViewer } from '@/lib/overtime'; +import { calcDuration, filterOvertimeRecordsForExportMonth, filterOvertimeRecordsForViewer } from '@/lib/overtime'; import type { OvertimeRecord } from '@/lib/overtime'; import { Pagination, usePagination } from '@/components/Pagination'; import { DictDrawer } from '@/components/requirement/DictDrawer'; @@ -133,7 +133,9 @@ function OvertimePageContent() { const handleExport = () => { const header = ['项目', '版本', '加班人', '开始时间', '结束时间', '时长(h)', '加班原因', '备注']; - const rows = filtered.map((r) => [ + const exportRecords = filterOvertimeRecordsForExportMonth(visibleRecords, monthFilter) + .sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()); + const rows = exportRecords.map((r) => [ projectName(r.projectId), versionName(r.versionId), r.person, diff --git a/apps/web/lib/member-page-header.test.ts b/apps/web/lib/member-page-header.test.ts new file mode 100644 index 0000000..5d6bef3 --- /dev/null +++ b/apps/web/lib/member-page-header.test.ts @@ -0,0 +1,28 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +test('members table pins each header cell above scrolling rows', () => { + const source = readFileSync(join(process.cwd(), 'app/admin/members/page.tsx'), 'utf8'); + const headerClassNames = Array.from( + source.matchAll(//g), + (match) => match[1], + ); + + assert.equal(headerClassNames.length, 6); + for (const className of headerClassNames) { + assert.match(className, /\bsticky\b/); + assert.match(className, /\btop-0\b/); + assert.match(className, /\bz-20\b/); + assert.match(className, /bg-\[var\(--bg-subtle\)\]/); + } +}); + +test('members table scrolls inside the table card instead of the padded page area', () => { + const source = readFileSync(join(process.cwd(), 'app/admin/members/page.tsx'), 'utf8'); + + assert.match(source, /className="flex-1 overflow-hidden bg-\[var\(--bg\)\] px-5 py-4"/); + assert.match(source, /className="h-full overflow-hidden rounded-2xl border border-\[var\(--line\)\] bg-\[var\(--bg-card\)\] shadow-\[var\(--shadow-sm\)\]"/); + assert.match(source, /className="h-full overflow-y-auto"/); +}); diff --git a/apps/web/lib/overtime.test.ts b/apps/web/lib/overtime.test.ts index 9b6a7bf..a1c8928 100644 --- a/apps/web/lib/overtime.test.ts +++ b/apps/web/lib/overtime.test.ts @@ -1,12 +1,13 @@ -import assert from 'node:assert/strict'; import test from 'node:test'; +import assert from 'node:assert/strict'; import { + filterOvertimeRecordsForExportMonth, filterOvertimeRecordsForViewer, type OvertimeRecord, } from './overtime'; -import type { Department, Member, RoleItem } from './members'; import type { AuthUser } from './auth-user'; +import type { Department, Member, RoleItem } from './members'; const departments: Department[] = [ { id: 'dept-tech', name: 'Tech', order: 1, createdAt: '2024-01-01' }, @@ -28,6 +29,29 @@ const records: OvertimeRecord[] = [ record('ot-pm', 'PM Carol'), ]; +const exportRecords: OvertimeRecord[] = [ + { + id: 'july-1', + projectId: 'project-1', + person: 'Alice', + startTime: '2026-07-01T19:00:00', + endTime: '2026-07-01T21:00:00', + duration: 2, + reasonId: 'reason-1', + createdAt: '2026-07-01', + }, + { + id: 'august-1', + projectId: 'project-1', + person: 'Bob', + startTime: '2026-08-02T19:00:00', + endTime: '2026-08-02T21:00:00', + duration: 2, + reasonId: 'reason-1', + createdAt: '2026-08-02', + }, +]; + test('filterOvertimeRecordsForViewer limits non-admin viewers to their department tree', () => { const visible = filterOvertimeRecordsForViewer(records, { viewer: toAuthUser(members[0]), @@ -50,6 +74,18 @@ test('filterOvertimeRecordsForViewer lets wildcard admin view every department', assert.deepEqual(visible.map((item) => item.id), ['ot-front', 'ot-back', 'ot-pm']); }); +test('filters export records by selected month', () => { + const result = filterOvertimeRecordsForExportMonth(exportRecords, '2026-07'); + + assert.deepEqual(result.map((record) => record.id), ['july-1']); +}); + +test('keeps all export records when all months are selected', () => { + const result = filterOvertimeRecordsForExportMonth(exportRecords, ''); + + assert.deepEqual(result.map((record) => record.id), ['july-1', 'august-1']); +}); + function member(id: string, name: string, departmentId: string, roleId: string): Member { return { id, diff --git a/apps/web/lib/overtime.ts b/apps/web/lib/overtime.ts index ee2ebe4..7b022d0 100644 --- a/apps/web/lib/overtime.ts +++ b/apps/web/lib/overtime.ts @@ -118,6 +118,14 @@ export function calcDuration(start: string, end: string): number { return Math.round(total * 10) / 10; } +export function filterOvertimeRecordsForExportMonth( + records: OvertimeRecord[], + monthFilter: string, +): OvertimeRecord[] { + if (!monthFilter) return [...records]; + return records.filter((record) => record.startTime.slice(0, 7) === monthFilter); +} + function collectDepartmentTreeIds(departments: Department[], departmentId: string): Set { const ids = new Set([departmentId]); let changed = true;