fix(前端): 修复成员表头与加班导出

关键改动:

- 成员管理表格改为卡片内滚动,并固定每个表头单元格

- 加班导出按当前月份筛选导出记录

- 增加成员表头与加班导出月份过滤测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-07-02 17:02:54 +08:00
parent 58b9f993ea
commit e906daf228
5 changed files with 117 additions and 41 deletions

View File

@@ -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(/<th className="([^"]+)">/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"/);
});

View File

@@ -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,

View File

@@ -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<string> {
const ids = new Set<string>([departmentId]);
let changed = true;