feat: enhance operations dashboard and reporting controls

This commit is contained in:
hectorzhao
2026-09-04 16:26:22 +08:00
parent bc18c7ff12
commit 48d0363920
23 changed files with 544 additions and 49 deletions
+7
View File
@@ -194,6 +194,8 @@ export const adminGovernanceApi = {
listDrainageFields: () => request<DictionaryItem[]>('/admin/dictionaries/drainage-fields'),
createDrainageField: (body: { code: string; name: string; fieldType: 'string' | 'image' | 'file'; required?: boolean; status?: string; description?: string }) =>
request<DictionaryItem>('/admin/dictionaries/drainage-fields', { method: 'POST', body: JSON.stringify(body) }),
updateDrainageField: (id: string, body: { code: string; name: string; fieldType: 'string' | 'image' | 'file'; description?: string }) =>
request<DictionaryItem>(`/admin/dictionaries/drainage-fields/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
deleteDrainageField: (id: string) => request<DictionaryItem>(`/admin/dictionaries/drainage-fields/${id}`, { method: 'DELETE' }),
listDrainageDetectionRules: (query: { keyword?: string; status?: string } = {}) =>
request<DrainageDetectionRule[]>(withQuery('/admin/dictionaries/drainage-detection-rules', query)),
@@ -208,6 +210,11 @@ export const adminGovernanceApi = {
listCommonReportFields: () => request<CommonReportField[]>('/admin/dictionaries/common-report-fields'),
createCommonReportField: (body: { drainageFieldId: string; reportType: 'signature' | 'drainage'; required: boolean; sortOrder?: number }) =>
request<CommonReportField>('/admin/dictionaries/common-report-fields', { method: 'POST', body: JSON.stringify(body) }),
reorderCommonReportFields: (body: { reportType: 'signature' | 'drainage'; ids: string[] }) =>
request<CommonReportField[]>('/admin/dictionaries/common-report-fields/order', {
method: 'PUT',
body: JSON.stringify(body),
}),
deleteCommonReportField: (id: string) => request<CommonReportField>(`/admin/dictionaries/common-report-fields/${id}`, { method: 'DELETE' }),
updateCommonReportField: (id: string, body: { drainageFieldId: string; reportType: 'signature' | 'drainage'; required: boolean }) =>
request<CommonReportField>(`/admin/dictionaries/common-report-fields/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
+17
View File
@@ -140,6 +140,23 @@ describe('request tenant and error boundaries', () => {
await expect(requestBlob('/client/file-fail')).rejects.toThrow('文件不存在');
});
it('marks string-body blob requests as JSON so download endpoints receive their payload', async () => {
let receivedContentType = '';
let receivedBody: unknown;
server.use(
http.post('http://localhost/api/admin/report-export', async ({ request: incoming }) => {
receivedContentType = incoming.headers.get('content-type') ?? '';
receivedBody = await incoming.json();
return new HttpResponse('xlsx-data', { status: 200 });
}),
);
await expect(
(await requestBlob('/admin/report-export', { method: 'POST', body: JSON.stringify({ reportType: 'signature' }) })).text(),
).resolves.toBe('xlsx-data');
expect(receivedContentType).toContain('application/json');
expect(receivedBody).toEqual({ reportType: 'signature' });
});
it('retries blob downloads after recent authentication and supports admin tenant selection', async () => {
writeSession({
portal: 'admin',
+3
View File
@@ -114,6 +114,9 @@ export async function request<T>(path: string, options: RequestOptions = {}): Pr
export async function requestBlob(path: string, options: RequestOptions = {}): Promise<Blob> {
const headers = new Headers(options.headers);
if (typeof options.body === 'string' && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
const portal = requestPortal(path);
const session = portal ? readSession(portal) : null;
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
+6
View File
@@ -152,6 +152,12 @@ export type DashboardResponse = {
spendCents: number;
returnedCents: number;
billingUnits: number;
segmentCount: number;
deliveredSegmentCount: number;
arrivalRate: number;
billedCents: number;
profitCents: number;
profitRate: number;
};
uplinkCount: number;
billing: { _count: { _all: number }; _sum: { amountCents?: number | null; billingUnits?: number | null } };