feat: deliver first finance preview with setup wizard
Release / release (push) Failing after 18s

This commit is contained in:
Qiufeng
2026-08-17 14:08:40 +08:00
parent 47403fd337
commit c8b0151769
558 changed files with 140774 additions and 3 deletions
+405
View File
@@ -0,0 +1,405 @@
import type { AxiosResponse } from 'axios';
import { request } from '@/utils/request';
import { commandHeaders } from './command';
export type AccountingTab = 'pending' | 'draft' | 'review' | 'exported' | 'result' | 'completed' | 'reversed' | 'void';
export type EventType = 'RECEIPT' | 'INVOICE' | 'PAYABLE' | 'PAYMENT' | 'PAYMENT_REFUND';
export type VoucherStatus =
| 'DRAFT'
| 'REVIEWING'
| 'RETURNED'
| 'APPROVED'
| 'EXPORTED'
| 'RESULT_RECORDED'
| 'COMPLETED'
| 'RESULT_REVERSED'
| 'VOID';
export type AccountingAction =
| 'GENERATE_DRAFT'
| 'EDIT'
| 'SUBMIT'
| 'APPROVE'
| 'RETURN'
| 'EXPORT'
| 'RECORD_RESULT'
| 'VERIFY_RESULT'
| 'REVERSE_RESULT'
| 'REOPEN_RESULT'
| 'VOID';
export interface PageMeta {
page: number;
size: number;
totalElements: number;
totalPages: number;
}
export interface Reference {
publicId: string;
code?: string | null;
name: string;
}
export interface AccountReference {
publicId: string;
code: string;
name: string;
accountType: string;
auxiliaryRequired: boolean;
}
export interface AccountingEvent {
publicId: string;
businessNo: string;
eventType: EventType;
sourceType: string;
sourcePublicId: string;
sourceBusinessNo?: string | null;
sourceVersion: string;
eventSequence: number;
sourceEventKey: string;
company: Reference;
project?: Reference | null;
counterparty?: Reference | null;
businessDate: string;
period: string;
amount: string;
currency: string;
status: string;
voucherPublicId?: string | null;
voucherStatus?: VoucherStatus | null;
version: number;
createdAt: string;
allowedActions: AccountingAction[];
}
export interface VoucherEntry {
publicId: string;
lineNo: number;
direction: 'DEBIT' | 'CREDIT';
account: Reference;
amount: string;
summary: string;
auxiliaryJson?: string | null;
}
export interface Voucher {
publicId: string;
businessNo: string;
eventPublicId: string;
eventBusinessNo: string;
company: Reference;
project?: Reference | null;
period: string;
businessDate: string;
summary: string;
debitTotal: string;
creditTotal: string;
balanceStatus: 'BALANCED' | 'UNBALANCED';
status: VoucherStatus;
submittedByName?: string | null;
reviewedByName?: string | null;
exportedAt?: string | null;
exportBatchNo?: string | null;
exportSha256?: string | null;
exportFileId?: string | null;
ruleVersion: string;
externalVoucherNo?: string | null;
resultAt?: string | null;
resultCycleNo: number;
evidenceFileIds: string[];
resultRecordedByName?: string | null;
resultRecordedAt?: string | null;
resultRecordRemark?: string | null;
resultVerifiedByName?: string | null;
resultVerifiedAt?: string | null;
resultVerifyOpinion?: string | null;
voidReason?: string | null;
version: number;
createdAt: string;
updatedAt: string;
allowedActions: AccountingAction[];
}
export interface VoucherDetail {
voucher: Voucher;
event: AccountingEvent;
entries: VoucherEntry[];
evidenceFiles: ResultEvidenceFile[];
resultActions: VoucherResultAction[];
}
export interface ResultEvidenceFile {
publicId: string;
originalName: string;
mediaType: string;
sizeBytes: number;
sha256: string;
scanStatus: string;
}
export interface VoucherResultAction {
publicId: string;
sequenceNo: number;
resultCycleNo: number;
actionType: 'REVERSE' | 'REOPEN';
reason: string;
externalVoucherNo?: string | null;
resultAt?: string | null;
resultRecordedByName?: string | null;
resultRecordedAt?: string | null;
resultRecordRemark?: string | null;
resultVerifiedByName?: string | null;
resultVerifiedAt?: string | null;
resultVerifyOpinion?: string | null;
evidenceFiles: ResultEvidenceFile[];
operatedByName: string;
operatedAt: string;
versionBefore: number;
versionAfter: number;
}
export interface VoucherExport {
voucher: Voucher;
batchNo: string;
sha256: string;
fileId: string;
rowCount: number;
}
export interface RecordAccountingResultPayload {
version: number;
externalVoucherNo: string;
resultAt: string;
evidenceFileIds: string[];
remark?: string;
}
export interface AccountingEventQuery {
period?: string;
projectId?: string;
eventType?: EventType;
keyword?: string;
sort: string;
page: number;
size: number;
}
export interface VoucherQuery extends AccountingEventQuery {
tab: Exclude<AccountingTab, 'pending'>;
status?: VoucherStatus;
externalVoucherNo?: string;
}
export interface VoucherEntryPayload {
lineNo: number;
direction: 'DEBIT' | 'CREDIT';
accountId: string;
amount: string;
summary: string;
auxiliaryJson?: string;
}
export interface VoucherUpdatePayload {
version: number;
period: string;
businessDate: string;
summary: string;
entries: VoucherEntryPayload[];
changeReason: string;
}
interface PageEnvelope<T> {
data: T[];
meta: PageMeta;
requestId: string;
}
async function page<T>(url: string, params: AccountingEventQuery | VoucherQuery) {
const envelope = await request.get<PageEnvelope<T>>({ url, params }, { isTransformResponse: false });
return {
items: Array.isArray(envelope?.data) ? envelope.data : [],
meta: envelope?.meta || { page: params.page, size: params.size, totalElements: 0, totalPages: 0 },
requestId: envelope?.requestId || '',
};
}
function normalizeEvent(event: AccountingEvent): AccountingEvent {
return { ...event, allowedActions: Array.isArray(event.allowedActions) ? event.allowedActions : [] };
}
function normalizeVoucher(voucher: Voucher): Voucher {
return {
...voucher,
allowedActions: Array.isArray(voucher.allowedActions) ? voucher.allowedActions : [],
evidenceFileIds: Array.isArray(voucher.evidenceFileIds) ? voucher.evidenceFileIds : [],
};
}
function normalizeVoucherDetail(detail: VoucherDetail): VoucherDetail {
return {
...detail,
voucher: normalizeVoucher(detail.voucher),
entries: Array.isArray(detail?.entries) ? detail.entries : [],
evidenceFiles: Array.isArray(detail?.evidenceFiles) ? detail.evidenceFiles : [],
resultActions: Array.isArray(detail?.resultActions)
? detail.resultActions.map((action) => ({
...action,
evidenceFiles: Array.isArray(action.evidenceFiles) ? action.evidenceFiles : [],
}))
: [],
event: normalizeEvent(detail.event),
};
}
export function getAccountingAccounts() {
return request
.get<AccountReference[]>({ url: '/accounting/accounts' })
.then((rows) => (Array.isArray(rows) ? rows : []));
}
export async function getAccountingEvents(params: AccountingEventQuery) {
const result = await page<AccountingEvent>('/accounting/events', params);
return { ...result, items: result.items.map(normalizeEvent) };
}
export async function getAccountingVouchers(params: VoucherQuery) {
const result = await page<Voucher>('/accounting/vouchers', params);
return { ...result, items: result.items.map(normalizeVoucher) };
}
export async function getAccountingVoucher(publicId: string) {
const detail = await request.get<VoucherDetail>({ url: `/accounting/vouchers/${publicId}` });
return normalizeVoucherDetail(detail);
}
export function generateVoucherDraft(publicId: string, version: number, idempotencyKey?: string) {
return request
.post<VoucherDetail>({
url: `/accounting/events/${publicId}/generate-draft`,
data: { version, ruleVersion: 'ACCOUNTING-RULE-V1' },
...commandHeaders('accounting-generate-draft', idempotencyKey),
})
.then(normalizeVoucherDetail);
}
export function updateAccountingVoucher(publicId: string, payload: VoucherUpdatePayload, idempotencyKey?: string) {
return request
.patch<VoucherDetail>({
url: `/accounting/vouchers/${publicId}`,
data: payload,
...commandHeaders('accounting-update', idempotencyKey),
})
.then(normalizeVoucherDetail);
}
export function submitAccountingVoucher(publicId: string, version: number, idempotencyKey?: string) {
return request
.post<VoucherDetail>({
url: `/accounting/vouchers/${publicId}/submit`,
data: { version },
...commandHeaders('accounting-submit', idempotencyKey),
})
.then(normalizeVoucherDetail);
}
export function approveAccountingVoucher(publicId: string, version: number, idempotencyKey?: string) {
return request
.post<VoucherDetail>({
url: `/accounting/vouchers/${publicId}/approve`,
data: { version },
...commandHeaders('accounting-approve', idempotencyKey),
})
.then(normalizeVoucherDetail);
}
export function returnAccountingVoucher(publicId: string, version: number, opinion: string, idempotencyKey?: string) {
return request
.post<VoucherDetail>({
url: `/accounting/vouchers/${publicId}/return`,
data: { version, opinion },
...commandHeaders('accounting-return', idempotencyKey),
})
.then(normalizeVoucherDetail);
}
export function exportAccountingVoucher(publicId: string, version: number, idempotencyKey?: string) {
return request.post<VoucherExport>({
url: `/accounting/vouchers/${publicId}/export`,
data: { version },
...commandHeaders('accounting-export', idempotencyKey),
});
}
export function downloadAccountingVoucherExport(publicId: string) {
return request.get<AxiosResponse<Blob>>(
{ url: `/accounting/vouchers/${publicId}/export-file`, responseType: 'blob' },
{ isReturnNativeResponse: true },
);
}
export function downloadAccountingVoucherResultEvidence(voucherPublicId: string, filePublicId: string) {
return request.get<AxiosResponse<Blob>>(
{
url: `/accounting/vouchers/${voucherPublicId}/result-files/${filePublicId}/content`,
responseType: 'blob',
},
{ isReturnNativeResponse: true },
);
}
export function recordAccountingResult(
publicId: string,
payload: RecordAccountingResultPayload,
idempotencyKey?: string,
) {
return request
.post<VoucherDetail>({
url: `/accounting/vouchers/${publicId}/record-result`,
data: payload,
...commandHeaders('accounting-record-result', idempotencyKey),
})
.then(normalizeVoucherDetail);
}
export function verifyAccountingResult(publicId: string, version: number, opinion: string, idempotencyKey?: string) {
return request
.post<VoucherDetail>({
url: `/accounting/vouchers/${publicId}/verify-result`,
data: { version, opinion },
...commandHeaders('accounting-verify-result', idempotencyKey),
})
.then(normalizeVoucherDetail);
}
export function reverseAccountingResult(publicId: string, version: number, reason: string, idempotencyKey?: string) {
return request
.post<VoucherDetail>({
url: `/accounting/vouchers/${publicId}/reverse-result`,
data: { version, reason },
...commandHeaders('accounting-reverse-result', idempotencyKey),
})
.then(normalizeVoucherDetail);
}
export function reopenAccountingResult(publicId: string, version: number, reason: string, idempotencyKey?: string) {
return request
.post<VoucherDetail>({
url: `/accounting/vouchers/${publicId}/reopen-result`,
data: { version, reason },
...commandHeaders('accounting-reopen-result', idempotencyKey),
})
.then(normalizeVoucherDetail);
}
export function voidAccountingVoucher(publicId: string, version: number, reason: string, idempotencyKey?: string) {
return request
.post<VoucherDetail>({
url: `/accounting/vouchers/${publicId}/void`,
data: { version, reason },
...commandHeaders('accounting-void', idempotencyKey),
})
.then(normalizeVoucherDetail);
}
+555
View File
@@ -0,0 +1,555 @@
import type { AxiosResponse } from 'axios';
import { request } from '@/utils/request';
import { commandHeaders } from './command';
export const ARCHIVE_API_BASE = '/archives';
const array = <T>(value: T[] | null | undefined): T[] => (Array.isArray(value) ? value : []);
export interface ArchivePageMeta {
page: number;
size: number;
totalElements: number;
totalPages: number;
}
export interface ArchiveReference {
publicId: string;
businessNo: string;
name: string;
}
export interface ArchivePackageItem {
publicId: string;
itemType: string;
itemName: string;
objectType?: string | null;
objectPublicId?: string | null;
filePublicId?: string | null;
fileName?: string | null;
fileSha256?: string | null;
required: boolean;
status: string;
missingReason?: string | null;
returnReason?: string | null;
notApplicableReason?: string | null;
notApplicableRequestedByName?: string | null;
notApplicableRequestedAt?: string | null;
notApplicableReviewedByName?: string | null;
notApplicableReviewedAt?: string | null;
notApplicableReviewOpinion?: string | null;
allowedActions: string[];
}
export interface ArchivePackageAction {
sequenceNo: number;
actionCode: string;
fromStatus?: string | null;
toStatus?: string | null;
opinion?: string | null;
actorName?: string | null;
occurredAt: string;
manifestSha256?: string | null;
}
export interface ArchivePackageObjectSnapshot {
publicId: string;
itemType: string;
objectType: string;
objectPublicId: string;
objectBusinessNo?: string | null;
objectName?: string | null;
objectStatus: string;
objectVersion?: number | null;
objectFormType?: string | null;
sourceVersionPublicId?: string | null;
snapshotSha256: string;
capturedAt: string;
}
export interface ArchivePackage {
publicId: string;
businessNo: string;
company: ArchiveReference;
project: ArchiveReference;
packageVersion: number;
rootPackagePublicId?: string | null;
supersedesPackagePublicId?: string | null;
revisionReason?: string | null;
ruleVersion: string;
completeness: string;
missingCount: number;
retentionYears: number;
retentionExpiresOn?: string | null;
retentionStatus: 'PENDING' | 'ACTIVE' | 'DUE_SOON' | 'EXPIRED' | 'FROZEN';
physicalLocation?: string | null;
frozen: boolean;
status: string;
manifestSha256?: string | null;
submittedByName?: string | null;
reviewedByName?: string | null;
submittedAt?: string | null;
archivedAt?: string | null;
updatedAt: string;
version: number;
allowedActions: string[];
items: ArchivePackageItem[];
objectSnapshots: ArchivePackageObjectSnapshot[];
actions: ArchivePackageAction[];
}
export interface ArchiveFile {
publicId: string;
displayName: string;
mediaType: string;
originalType?: string | null;
sizeBytes: number;
sha256: string;
scanStatus: string;
companyPublicId: string;
companyName: string;
projectPublicId: string;
projectBusinessNo: string;
projectName: string;
contractPublicId?: string | null;
counterpartyPublicId?: string | null;
formType?: string | null;
documentType: string;
seriesPublicId: string;
versionNo: number;
archiveStatus: string;
borrowStatus?: string | null;
packagePublicId?: string | null;
packageStatus?: string | null;
uploadedBy: string;
uploadedAt: string;
sensitive: boolean;
allowedActions: string[];
}
export interface ArchiveFileDetail {
file: ArchiveFile;
versions: ArchiveFile[];
borrows: ArchiveBorrow[];
}
export interface ArchiveBorrow {
publicId: string;
businessNo: string;
filePublicId: string;
fileName: string;
projectPublicId: string;
projectName: string;
purpose: string;
dueAt: string;
status: string;
applicantName: string;
approvedByName?: string | null;
approvedAt?: string | null;
reviewOpinion?: string | null;
returnCondition?: string | null;
returnedByName?: string | null;
returnedAt?: string | null;
version: number;
allowedActions: string[];
}
export interface ArchivePackageQuery {
view?: 'prepare' | 'check' | 'returned' | 'archived';
companyId?: string;
projectId?: string;
status?: string;
completeness?: string;
frozen?: boolean;
keyword?: string;
sort?: string;
page: number;
size: number;
}
export interface ArchiveFileQuery {
companyId?: string;
projectId?: string;
contractId?: string;
counterpartyId?: string;
formType?: string;
fileName?: string;
uploadedBy?: string;
uploadedDateFrom?: string;
uploadedDateTo?: string;
originalType?: string;
archiveStatus?: string;
scanStatus?: string;
sort?: string;
page: number;
size: number;
}
export interface ArchiveBorrowQuery {
status?: string;
keyword?: string;
page: number;
size: number;
}
export interface ArchivePackageCreatePayload {
projectId: string;
retentionYears?: number;
physicalLocation?: string;
}
export interface ArchivePackageCommandPayload {
version: number;
opinion?: string;
returnItemIds?: string[];
}
export interface ArchiveFreezePayload {
version: number;
reason: string;
}
export interface ArchivePackageRevisionPayload {
version: number;
reason: string;
}
export interface ArchiveNotApplicablePayload {
version: number;
reason: string;
}
export interface ArchiveNotApplicableReviewPayload {
version: number;
approved: boolean;
opinion: string;
}
export interface ArchiveFileMetadata {
projectId: string;
packageId?: string;
packageItemId?: string;
contractId?: string;
counterpartyId?: string;
formType?: string;
fileType: string;
originalType?: string;
replacesFileId?: string;
replacementReason?: string;
notApplicableReason?: string;
}
export interface ArchiveBorrowCreatePayload {
fileId: string;
purpose: string;
dueAt: string;
}
export interface ArchiveBorrowCommandPayload {
version: number;
opinion?: string;
returnCondition?: string;
}
interface PageEnvelope<T> {
data: T[];
meta: ArchivePageMeta;
requestId: string;
}
export function normalizeArchivePackageItem(item: Partial<ArchivePackageItem> | null | undefined): ArchivePackageItem {
return { ...(item || {}), allowedActions: array(item?.allowedActions) } as ArchivePackageItem;
}
export function normalizeArchivePackage(value: Partial<ArchivePackage> | null | undefined): ArchivePackage {
const item = value || {};
return {
...item,
allowedActions: array(item.allowedActions),
items: array(item.items).map(normalizeArchivePackageItem),
objectSnapshots: array(item.objectSnapshots),
actions: array(item.actions),
} as ArchivePackage;
}
export function normalizeArchiveFile(value: Partial<ArchiveFile> | null | undefined): ArchiveFile {
return { ...(value || {}), allowedActions: array(value?.allowedActions) } as ArchiveFile;
}
export function normalizeArchiveBorrow(value: Partial<ArchiveBorrow> | null | undefined): ArchiveBorrow {
return { ...(value || {}), allowedActions: array(value?.allowedActions) } as ArchiveBorrow;
}
export function normalizeArchiveFileDetail(value: Partial<ArchiveFileDetail> | null | undefined): ArchiveFileDetail {
return {
...(value || {}),
file: normalizeArchiveFile(value?.file),
versions: array(value?.versions).map(normalizeArchiveFile),
borrows: array(value?.borrows).map(normalizeArchiveBorrow),
} as ArchiveFileDetail;
}
export function archiveErrorText(error: unknown, fallback: string): string {
const value = error as Error & { status?: number; code?: string; fieldErrors?: Record<string, string> };
const fields = value.fieldErrors ? Object.values(value.fieldErrors).join(';') : '';
if (value.status === 401) return '登录会话已失效,请重新登录后重试。';
if (value.status === 403) return '当前身份没有访问该数据或执行该操作的权限。';
if (value.status === 404) return '记录不存在、已被删除或当前身份无权查看。';
if (value.code === 'VERSION_CONFLICT' || value.status === 409)
return value.message || '数据已发生变化,请刷新后重试。';
if (value.status === 422) {
return fields
? `${value.message || '当前条件不满足业务规则'}:${fields}`
: value.message || '当前条件不满足业务规则,请检查后重试。';
}
if (value.status && value.status >= 500) return value.message || '服务处理失败,请稍后重试。';
return fields ? `${value.message || fallback}:${fields}` : value.message || fallback;
}
async function page<T>(url: string, params: Record<string, unknown>) {
const envelope = await request.get<PageEnvelope<T>>({ url, params }, { isTransformResponse: false });
return {
items: array(envelope.data),
meta: envelope.meta || {
page: Number(params.page) || 1,
size: Number(params.size) || 20,
totalElements: 0,
totalPages: 0,
},
requestId: envelope.requestId,
};
}
export function getArchivePackages(params: ArchivePackageQuery) {
return page<ArchivePackage>(`${ARCHIVE_API_BASE}/packages`, params as unknown as Record<string, unknown>).then(
(result) => ({
...result,
items: result.items.map(normalizeArchivePackage),
}),
);
}
export function getArchivePackage(publicId: string) {
return request.get<ArchivePackage>({ url: `${ARCHIVE_API_BASE}/packages/${publicId}` }).then(normalizeArchivePackage);
}
export function createArchivePackage(payload: ArchivePackageCreatePayload, idempotencyKey?: string) {
return request
.post<ArchivePackage>({
url: `${ARCHIVE_API_BASE}/packages`,
data: payload,
...commandHeaders('archive-package-create', idempotencyKey),
})
.then(normalizeArchivePackage);
}
export function checkArchivePackage(publicId: string, payload: ArchivePackageCommandPayload, idempotencyKey?: string) {
return request
.post<ArchivePackage>({
url: `${ARCHIVE_API_BASE}/packages/${publicId}/check`,
data: payload,
...commandHeaders('archive-package-check', idempotencyKey),
})
.then(normalizeArchivePackage);
}
export function submitArchivePackage(publicId: string, payload: ArchivePackageCommandPayload, idempotencyKey?: string) {
return request
.post<ArchivePackage>({
url: `${ARCHIVE_API_BASE}/packages/${publicId}/submit`,
data: payload,
...commandHeaders('archive-package-submit', idempotencyKey),
})
.then(normalizeArchivePackage);
}
export function returnArchivePackage(publicId: string, payload: ArchivePackageCommandPayload, idempotencyKey?: string) {
return request
.post<ArchivePackage>({
url: `${ARCHIVE_API_BASE}/packages/${publicId}/return`,
data: payload,
...commandHeaders('archive-package-return', idempotencyKey),
})
.then(normalizeArchivePackage);
}
export function reviseArchivePackage(
publicId: string,
payload: ArchivePackageRevisionPayload,
idempotencyKey?: string,
) {
return request
.post<ArchivePackage>({
url: `${ARCHIVE_API_BASE}/packages/${publicId}/revise`,
data: payload,
...commandHeaders('archive-package-revise', idempotencyKey),
})
.then(normalizeArchivePackage);
}
export function archiveArchivePackage(
publicId: string,
payload: ArchivePackageCommandPayload,
idempotencyKey?: string,
) {
return request
.post<ArchivePackage>({
url: `${ARCHIVE_API_BASE}/packages/${publicId}/archive`,
data: payload,
...commandHeaders('archive-package-archive', idempotencyKey),
})
.then(normalizeArchivePackage);
}
export function freezeArchivePackage(publicId: string, payload: ArchiveFreezePayload, idempotencyKey?: string) {
return request
.post<ArchivePackage>({
url: `${ARCHIVE_API_BASE}/packages/${publicId}/freeze`,
data: payload,
...commandHeaders('archive-package-freeze', idempotencyKey),
})
.then(normalizeArchivePackage);
}
export function unfreezeArchivePackage(
publicId: string,
payload: ArchivePackageCommandPayload,
idempotencyKey?: string,
) {
return request
.post<ArchivePackage>({
url: `${ARCHIVE_API_BASE}/packages/${publicId}/unfreeze`,
data: payload,
...commandHeaders('archive-package-unfreeze', idempotencyKey),
})
.then(normalizeArchivePackage);
}
export function markArchiveItemNotApplicable(
packageId: string,
itemId: string,
payload: ArchiveNotApplicablePayload,
idempotencyKey?: string,
) {
return request
.post<ArchivePackage>({
url: `${ARCHIVE_API_BASE}/packages/${packageId}/items/${itemId}/not-applicable`,
data: payload,
...commandHeaders('archive-item-not-applicable', idempotencyKey),
})
.then(normalizeArchivePackage);
}
export function reviewArchiveItemNotApplicable(
packageId: string,
itemId: string,
payload: ArchiveNotApplicableReviewPayload,
idempotencyKey?: string,
) {
return request
.post<ArchivePackage>({
url: `${ARCHIVE_API_BASE}/packages/${packageId}/items/${itemId}/not-applicable/review`,
data: payload,
...commandHeaders('archive-item-not-applicable-review', idempotencyKey),
})
.then(normalizeArchivePackage);
}
export function getArchiveFiles(params: ArchiveFileQuery) {
return page<ArchiveFile>(`${ARCHIVE_API_BASE}/files`, params as unknown as Record<string, unknown>).then(
(result) => ({
...result,
items: result.items.map(normalizeArchiveFile),
}),
);
}
export function getArchiveFile(publicId: string) {
return request
.get<ArchiveFileDetail>({ url: `${ARCHIVE_API_BASE}/files/${publicId}` })
.then(normalizeArchiveFileDetail);
}
export function rescanArchiveFile(publicId: string, idempotencyKey?: string) {
return request
.post<ArchiveFile>({
url: `${ARCHIVE_API_BASE}/files/${publicId}/rescan`,
data: {},
...commandHeaders('archive-file-rescan', idempotencyKey),
})
.then(normalizeArchiveFile);
}
export function getArchiveFileContent(publicId: string, preview = false) {
return request.get<AxiosResponse<Blob>>(
{ url: `${ARCHIVE_API_BASE}/files/${publicId}/content`, params: { preview }, responseType: 'blob' },
{ isReturnNativeResponse: true },
);
}
export function uploadArchiveFile(metadata: ArchiveFileMetadata, file: File, idempotencyKey?: string) {
const formData = new FormData();
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
formData.append('file', file, file.name);
return request
.post<ArchiveFile>({
url: `${ARCHIVE_API_BASE}/files`,
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
...commandHeaders('archive-file-upload', idempotencyKey).headers,
},
})
.then(normalizeArchiveFile);
}
export function getArchiveBorrows(params: ArchiveBorrowQuery) {
return page<ArchiveBorrow>(`${ARCHIVE_API_BASE}/borrows`, params as unknown as Record<string, unknown>).then(
(result) => ({
...result,
items: result.items.map(normalizeArchiveBorrow),
}),
);
}
export function createArchiveBorrow(payload: ArchiveBorrowCreatePayload, idempotencyKey?: string) {
return request
.post<ArchiveBorrow>({
url: `${ARCHIVE_API_BASE}/borrows`,
data: payload,
...commandHeaders('archive-borrow-create', idempotencyKey),
})
.then(normalizeArchiveBorrow);
}
export function approveArchiveBorrow(publicId: string, payload: ArchiveBorrowCommandPayload, idempotencyKey?: string) {
return request
.post<ArchiveBorrow>({
url: `${ARCHIVE_API_BASE}/borrows/${publicId}/approve`,
data: payload,
...commandHeaders('archive-borrow-approve', idempotencyKey),
})
.then(normalizeArchiveBorrow);
}
export function rejectArchiveBorrow(publicId: string, payload: ArchiveBorrowCommandPayload, idempotencyKey?: string) {
return request
.post<ArchiveBorrow>({
url: `${ARCHIVE_API_BASE}/borrows/${publicId}/reject`,
data: payload,
...commandHeaders('archive-borrow-reject', idempotencyKey),
})
.then(normalizeArchiveBorrow);
}
export function returnArchiveBorrow(publicId: string, payload: ArchiveBorrowCommandPayload, idempotencyKey?: string) {
return request
.post<ArchiveBorrow>({
url: `${ARCHIVE_API_BASE}/borrows/${publicId}/return`,
data: payload,
...commandHeaders('archive-borrow-return', idempotencyKey),
})
.then(normalizeArchiveBorrow);
}
+108
View File
@@ -0,0 +1,108 @@
import { request } from '@/utils/request';
export interface AuditPageMeta {
page: number;
size: number;
totalElements: number;
totalPages: number;
}
export type AuditSort = 'occurredAt,asc' | 'occurredAt,desc' | 'eventSequence,asc' | 'eventSequence,desc';
export interface AuditQuery {
occurredFrom?: string;
occurredTo?: string;
actorId?: string;
identityCode?: string;
objectType?: string;
objectId?: string;
action?: string;
requestId?: string;
result?: string;
keyword?: string;
sort: AuditSort;
page: number;
size: number;
}
export interface AuditLog {
publicId: string;
eventSequence: number;
requestId: string;
userPublicId?: string | null;
username: string;
activeRole?: string | null;
companyPublicId?: string | null;
projectPublicId?: string | null;
actionCode: string;
objectType: string;
objectPublicId?: string | null;
resultCode: string;
reason?: string | null;
beforeJson?: string | null;
afterJson?: string | null;
occurredAt: string;
allowedActions: string[];
}
export interface AuditExport {
exportId: string;
rowCount: number;
sha256: string;
fileName: string;
content: string;
}
function requiredString(value: unknown, field: string): string {
if (typeof value !== 'string' || !value.trim()) throw new Error(`审计导出响应缺少${field}`);
return value;
}
function nonNegativeInteger(value: unknown, field: string): number {
if (!Number.isInteger(value) || Number(value) < 0) throw new Error(`审计导出响应中的${field}无效`);
return Number(value);
}
export function normalizeAuditExport(value: AuditExport): AuditExport {
return {
exportId: requiredString(value?.exportId, '导出编号'),
rowCount: nonNegativeInteger(value?.rowCount, '导出行数'),
sha256: requiredString(value?.sha256, '文件摘要'),
fileName: requiredString(value?.fileName, '文件名'),
content: requiredString(value?.content, '文件内容'),
};
}
interface AuditEnvelope {
data: AuditLog[];
meta: AuditPageMeta;
requestId: string;
}
export async function getAuditLogs(params: AuditQuery) {
const envelope = await request.get<AuditEnvelope>(
{ url: '/audit/logs', params },
{ isTransformResponse: false, joinTime: false },
);
return {
items: Array.isArray(envelope?.data)
? envelope.data.map((row) => ({
...row,
allowedActions: Array.isArray(row?.allowedActions) ? row.allowedActions : [],
}))
: [],
meta: envelope?.meta || { page: params.page, size: params.size, totalElements: 0, totalPages: 0 },
requestId: envelope?.requestId || '',
};
}
export function getAuditLog(publicId: string) {
return request.get<AuditLog>({ url: `/audit/logs/${publicId}` }, { joinTime: false }).then((row) => ({
...row,
allowedActions: Array.isArray(row?.allowedActions) ? row.allowedActions : [],
}));
}
export function exportAuditLogs(payload: Omit<AuditQuery, 'page' | 'size'>) {
return request.post<AuditExport>({ url: '/audit/exports', data: payload }).then(normalizeAuditExport);
}
+177
View File
@@ -0,0 +1,177 @@
import { request } from '@/utils/request';
export interface UserView {
publicId: string;
username: string;
displayName: string;
departmentName?: string;
mustChangePassword: boolean;
authorizationScopes?: AuthorizationScopeView[];
recentLogins?: RecentLoginView[];
}
function normalizeUser(user: UserView | null | undefined): UserView {
return {
publicId: user?.publicId || '',
username: user?.username || '',
displayName: user?.displayName || '',
departmentName: user?.departmentName || '',
mustChangePassword: Boolean(user?.mustChangePassword),
authorizationScopes: Array.isArray(user?.authorizationScopes) ? user.authorizationScopes : [],
recentLogins: Array.isArray(user?.recentLogins) ? user.recentLogins : [],
};
}
function normalizeRole(role: RoleView): RoleView {
return {
code: String(role?.code || ''),
name: role?.name || role?.code || '未命名身份',
description: role?.description,
workbenchRoute: role?.workbenchRoute,
};
}
function normalizeSession(session: SessionView): SessionView {
return {
...session,
authenticated: Boolean(session?.authenticated),
user: normalizeUser(session?.user),
roles: Array.isArray(session?.roles) ? session.roles.map(normalizeRole) : [],
permissions: Array.isArray(session?.permissions) ? session.permissions : [],
};
}
function normalizeSessionSummary(session: SessionSummaryView): SessionSummaryView {
return {
sessionId: String(session?.sessionId || ''),
createdAt: session?.createdAt || '',
lastAccessedAt: session?.lastAccessedAt || '',
expiresAt: session?.expiresAt || '',
current: Boolean(session?.current),
deviceSummary: session?.deviceSummary,
maskedIp: session?.maskedIp,
};
}
export interface AuthorizationScopeView {
rowKey?: string;
roleCode: string;
roleName: string;
scopeType: 'GLOBAL' | 'COMPANY' | 'PROJECT' | string;
companyPublicId?: string;
companyCode?: string;
companyName?: string;
projectPublicId?: string;
projectCode?: string;
projectName?: string;
permissionCount: number;
amountLimit?: number;
}
export interface RecentLoginView {
occurredAt: string;
deviceSummary: string;
maskedIp: string;
}
export interface RoleView {
code: string;
name: string;
description?: string;
workbenchRoute?: string;
}
export interface SessionView {
authenticated: boolean;
user: UserView;
roles: RoleView[];
activeRole?: string | null;
permissions: string[];
permissionVersion?: string;
sessionExpiresAt?: string;
absoluteSessionExpiresAt?: string;
}
export interface SessionSummaryView {
sessionId: string;
createdAt: string;
lastAccessedAt: string;
expiresAt: string;
current: boolean;
deviceSummary?: string;
maskedIp?: string;
}
export interface LoginRequest {
username: string;
password: string;
}
export interface PasswordChangeRequest {
currentPassword: string;
newPassword: string;
confirmPassword: string;
}
export interface CsrfView {
headerName: string;
parameterName: string;
token: string;
}
export function getCsrf() {
return request.get<CsrfView>({ url: '/auth/csrf' }, { isTransformResponse: true, retry: { count: 0, delay: 0 } });
}
export function login(payload: LoginRequest) {
return request
.post<SessionView>({ url: '/auth/login', data: payload }, { retry: { count: 0, delay: 0 } })
.then(normalizeSession);
}
export function getSession() {
return request.get<SessionView>({ url: '/auth/session' }).then(normalizeSession);
}
export function getRoles() {
return request
.get<RoleView[]>({ url: '/auth/roles' })
.then((roles) => (Array.isArray(roles) ? roles.map(normalizeRole) : []));
}
export function getProfile() {
return request.get<UserView>({ url: '/auth/profile' }).then(normalizeUser);
}
export function selectRole(roleCode: string) {
return request
.post<SessionView>({ url: '/auth/select-role', data: { roleCode } }, { retry: { count: 0, delay: 0 } })
.then(normalizeSession);
}
export function changePassword(payload: PasswordChangeRequest) {
return request
.patch<SessionView>({ url: '/auth/password', data: payload }, { retry: { count: 0, delay: 0 } })
.then(normalizeSession);
}
export function listSessions() {
return request
.get<SessionSummaryView[]>({ url: '/auth/sessions' })
.then((sessions) => (Array.isArray(sessions) ? sessions.map(normalizeSessionSummary) : []));
}
export function deleteSession(sessionId: string) {
return request.delete<void>(
{ url: `/auth/sessions/${encodeURIComponent(sessionId)}` },
{ retry: { count: 0, delay: 0 } },
);
}
export function deleteAllSessions() {
return request.delete<void>({ url: '/auth/sessions' }, { retry: { count: 0, delay: 0 } });
}
export function logout() {
return request.post<void>({ url: '/auth/logout' }, { retry: { count: 0, delay: 0 } });
}
+11
View File
@@ -0,0 +1,11 @@
function randomPart(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
/** Every mutating request gets a stable key for the lifetime of that call. */
export function commandHeaders(prefix: string, key?: string) {
return { headers: { 'Idempotency-Key': key ?? `${prefix}-${randomPart()}` } };
}
+315
View File
@@ -0,0 +1,315 @@
import type { AxiosResponse } from 'axios';
import { request } from '@/utils/request';
import { commandHeaders } from './command';
export interface ContractCostPageMeta {
page: number;
size: number;
totalElements: number;
totalPages: number;
}
interface ApiEnvelope<T> {
data: T;
meta?: ContractCostPageMeta;
requestId: string;
}
export interface ContractCostReference {
publicId: string;
code?: string | null;
name: string;
}
export interface ContractCostReferences {
companies: ContractCostReference[];
projects: ContractCostReference[];
counterparties: ContractCostReference[];
costCategories: ContractCostReference[];
}
export interface ContractCostQuery {
companyId?: string;
projectId?: string;
counterpartyId?: string;
keyword?: string;
status?: string;
costCategoryId?: string;
businessDateFrom?: string;
businessDateTo?: string;
sort: string;
page?: number;
size?: number;
}
export interface ContractCostLedgerItem {
publicId: string;
businessNo: string;
name: string;
company: ContractCostReference;
project: ContractCostReference;
counterparty: ContractCostReference;
originalAmount: string;
approvedChangeAmount: string;
settlementAmount?: string | null;
invoicedAmount: string;
pendingPayableAmount: string;
payableAmount: string;
paidAmount: string;
availableAmount: string;
currency: string;
status: string;
version: number;
updatedAt: string;
allowedActions: string[];
}
export interface ContractCostSummary {
contractCount: number;
originalAmount: string;
approvedChangeAmount: string;
settlementAmount: string;
invoicedAmount: string;
pendingPayableAmount: string;
payableAmount: string;
paidAmount: string;
availableAmount: string;
}
export interface ContractCostPage {
items: ContractCostLedgerItem[];
summary: ContractCostSummary;
}
export interface ContractPayableLine {
publicId: string;
businessNo: string;
businessDate: string;
costCategory?: ContractCostReference | null;
amount: string;
invoicedAmount: string;
paidAmount: string;
unpaidAmount: string;
status: string;
version: number;
updatedAt: string;
allowedActions: string[];
}
export interface ContractChangeLine {
publicId: string;
changeNo: string;
changeAmount: string;
effectiveDate: string;
reason: string;
status: string;
version: number;
createdByName: string;
createdAt: string;
allowedActions: string[];
}
export interface ContractSettlementLine {
publicId: string;
settlementNo: string;
settlementAmount: string;
settlementDate: string;
sourceDocumentId?: string | null;
status: string;
version: number;
createdByName: string;
createdAt: string;
allowedActions: string[];
}
export interface ContractInvoiceLine {
publicId: string;
businessNo: string;
requestedDate: string;
invoiceType: string;
amount: string;
taxRate?: string | null;
invoiceNo?: string | null;
issuedDate?: string | null;
status: string;
updatedAt: string;
}
export interface ContractPaymentLine {
publicId: string;
businessNo: string;
requestedDate: string;
requestedAmount: string;
approvedAmount?: string | null;
purpose?: string | null;
status: string;
updatedAt: string;
}
export interface ContractCostDetail {
contract: ContractCostLedgerItem;
changes: ContractChangeLine[];
settlements: ContractSettlementLine[];
payables: ContractPayableLine[];
invoices: ContractInvoiceLine[];
payments: ContractPaymentLine[];
}
function normalizeLedgerItem(item: ContractCostLedgerItem): ContractCostLedgerItem {
return { ...item, allowedActions: Array.isArray(item?.allowedActions) ? item.allowedActions : [] };
}
function normalizeContractCostDetail(detail: ContractCostDetail): ContractCostDetail {
return {
...detail,
contract: normalizeLedgerItem(detail.contract),
changes: (Array.isArray(detail?.changes) ? detail.changes : []).map((row) => ({
...row,
allowedActions: Array.isArray(row?.allowedActions) ? row.allowedActions : [],
})),
settlements: (Array.isArray(detail?.settlements) ? detail.settlements : []).map((row) => ({
...row,
allowedActions: Array.isArray(row?.allowedActions) ? row.allowedActions : [],
})),
payables: (Array.isArray(detail?.payables) ? detail.payables : []).map((row) => ({
...row,
allowedActions: Array.isArray(row?.allowedActions) ? row.allowedActions : [],
})),
invoices: Array.isArray(detail?.invoices) ? detail.invoices : [],
payments: Array.isArray(detail?.payments) ? detail.payments : [],
};
}
export async function getContractCostPage(params: ContractCostQuery) {
const envelope = await request.get<ApiEnvelope<ContractCostPage>>(
{ url: '/contract-costs/contracts', params },
{ isTransformResponse: false },
);
return {
data: {
items: Array.isArray(envelope?.data?.items) ? envelope.data.items.map(normalizeLedgerItem) : [],
summary: envelope?.data?.summary || {
contractCount: 0,
originalAmount: '0.00',
approvedChangeAmount: '0.00',
settlementAmount: '0.00',
invoicedAmount: '0.00',
pendingPayableAmount: '0.00',
payableAmount: '0.00',
paidAmount: '0.00',
availableAmount: '0.00',
},
},
meta: envelope.meta || {
page: params.page || 1,
size: params.size || 20,
totalElements: 0,
totalPages: 0,
},
requestId: envelope.requestId,
};
}
export function getContractCostReferences() {
return request.get<ContractCostReferences>({ url: '/contract-costs/references' });
}
export function getContractCostDetail(publicId: string) {
return request
.get<ContractCostDetail>({ url: `/contract-costs/contracts/${publicId}` })
.then(normalizeContractCostDetail);
}
export interface ContractChangeCreatePayload {
changeNo: string;
changeAmount: string;
effectiveDate: string;
reason: string;
contractVersion: number;
}
export interface ContractSettlementCreatePayload {
settlementNo: string;
settlementAmount: string;
settlementDate: string;
sourceDocumentId: string;
contractVersion: number;
}
export interface ContractPayableCreatePayload {
businessNo: string;
costCategoryId: string;
sourceDocumentId: string;
businessDate: string;
amount: string;
contractVersion: number;
}
export function createContractChange(contractId: string, data: ContractChangeCreatePayload, idempotencyKey?: string) {
return request.post<ContractCostDetail>({
url: `/contract-costs/contracts/${contractId}/changes`,
data,
...commandHeaders('contract-cost-change-create', idempotencyKey),
});
}
export function voidContractChange(changeId: string, version: number, reason: string, idempotencyKey?: string) {
return request.post<ContractCostDetail>({
url: `/contract-costs/changes/${changeId}/void`,
data: { version, reason },
...commandHeaders('contract-cost-change-void', idempotencyKey),
});
}
export function createContractSettlement(
contractId: string,
data: ContractSettlementCreatePayload,
idempotencyKey?: string,
) {
return request.post<ContractCostDetail>({
url: `/contract-costs/contracts/${contractId}/settlements`,
data,
...commandHeaders('contract-cost-settlement-create', idempotencyKey),
});
}
export function voidContractSettlement(settlementId: string, version: number, reason: string, idempotencyKey?: string) {
return request.post<ContractCostDetail>({
url: `/contract-costs/settlements/${settlementId}/void`,
data: { version, reason },
...commandHeaders('contract-cost-settlement-void', idempotencyKey),
});
}
export function createContractPayable(contractId: string, data: ContractPayableCreatePayload, idempotencyKey?: string) {
return request.post<ContractCostDetail>({
url: `/contract-costs/contracts/${contractId}/payables`,
data,
...commandHeaders('contract-cost-payable-create', idempotencyKey),
});
}
export function confirmContractPayable(payableId: string, version: number, idempotencyKey?: string) {
return request.post<ContractCostDetail>({
url: `/contract-costs/payables/${payableId}/confirm`,
data: { version },
...commandHeaders('contract-cost-payable-confirm', idempotencyKey),
});
}
export function voidContractPayable(payableId: string, version: number, reason: string, idempotencyKey?: string) {
return request.post<ContractCostDetail>({
url: `/contract-costs/payables/${payableId}/void`,
data: { version, reason },
...commandHeaders('contract-cost-payable-void', idempotencyKey),
});
}
export function exportContractCostCsv(params: Omit<ContractCostQuery, 'page' | 'size'>) {
return request.get<AxiosResponse<Blob>>(
{ url: '/contract-costs/contracts/export', params, responseType: 'blob' },
{ isReturnNativeResponse: true },
);
}
+151
View File
@@ -0,0 +1,151 @@
import { request } from '@/utils/request';
export type GovernanceResource = 'users' | 'roles' | 'scopes' | 'templates' | 'parameters';
export interface GovernancePageMeta {
page: number;
size: number;
totalElements: number;
totalPages: number;
}
export interface GovernanceRow {
resource: GovernanceResource;
publicId: string;
username?: string;
displayName?: string;
departmentName?: string;
enabled?: boolean;
mustChangePassword?: boolean;
roleCodes?: string[];
code?: string;
name?: string;
description?: string;
sortOrder?: number;
memberCount?: number;
userPublicId?: string;
roleCode?: string;
permissionCode?: string;
scopeType?: string;
companyPublicId?: string;
projectPublicId?: string;
amountLimit?: string;
status?: string;
formType?: string;
templateVersion?: number;
schemaVersion?: number;
parameterGroup?: string;
versionNo?: number;
version?: number;
publishedAt?: string;
effectiveAt?: string;
createdAt?: string;
updatedAt?: string;
}
export interface GovernanceResourceView {
resource: GovernanceResource;
publicId: string;
businessCode: string;
status: string;
version: number;
values: Record<string, unknown>;
allowedActions: string[];
}
export interface GovernanceCreatePayload {
code?: string;
name?: string;
description?: string;
username?: string;
displayName?: string;
departmentName?: string;
password?: string;
roleCode?: string;
permissionCode?: string;
userId?: string;
scopeType?: string;
companyId?: string;
projectId?: string;
amountLimit?: number;
formType?: string;
templateVersion?: number;
schemaVersion?: number;
parameterGroup?: string;
versionNo?: number;
valueJson?: string;
roleCodes?: string[];
permissionCodes?: string[];
}
export interface GovernanceUpdatePayload extends GovernanceCreatePayload {
version: number;
enabled?: boolean;
}
export interface GovernanceCommandPayload {
version: number;
reason?: string;
}
interface ListEnvelope {
data: GovernanceRow[];
meta: GovernancePageMeta;
requestId: string;
}
function normalizeGovernanceView(view: GovernanceResourceView): GovernanceResourceView {
return {
...view,
values: view?.values && typeof view.values === 'object' ? view.values : {},
allowedActions: Array.isArray(view?.allowedActions) ? view.allowedActions : [],
};
}
export async function getGovernanceResource(
resource: GovernanceResource,
page: number,
size: number,
keyword?: string,
) {
const envelope = await request.get<ListEnvelope>(
{ url: `/admin/${resource}`, params: { keyword: keyword || undefined, page, size } },
{ isTransformResponse: false },
);
return {
items: Array.isArray(envelope?.data) ? envelope.data : [],
meta: envelope.meta || { page, size, totalElements: 0, totalPages: 0 },
requestId: envelope.requestId,
};
}
export function getGovernanceDetail(resource: GovernanceResource, publicId: string) {
return request.get<GovernanceResourceView>({ url: `/admin/${resource}/${publicId}` }).then(normalizeGovernanceView);
}
export function createGovernanceResource(resource: GovernanceResource, payload: GovernanceCreatePayload) {
return request
.post<GovernanceResourceView>({ url: `/admin/${resource}`, data: payload })
.then(normalizeGovernanceView);
}
export function updateGovernanceResource(
resource: GovernanceResource,
publicId: string,
payload: GovernanceUpdatePayload,
) {
return request
.patch<GovernanceResourceView>({ url: `/admin/${resource}/${publicId}`, data: payload })
.then(normalizeGovernanceView);
}
export function commandGovernanceResource(
resource: GovernanceResource,
publicId: string,
command: 'enable' | 'disable' | 'publish' | 'restore',
payload: GovernanceCommandPayload,
) {
return request
.post<GovernanceResourceView>({ url: `/admin/${resource}/${publicId}/${command}`, data: payload })
.then(normalizeGovernanceView);
}
+396
View File
@@ -0,0 +1,396 @@
import { request } from '@/utils/request';
export type MasterDataResource =
| 'companies'
| 'departments'
| 'counterparties'
| 'bank-accounts'
| 'contracts'
| 'cost-categories'
| 'accounts'
| 'tax-rates';
export type MasterDataAction = 'EDIT' | 'SUBMIT' | 'REVIEW' | 'DISABLE';
export type LegacyIdentifierResource = MasterDataResource | 'projects';
export type LegacyIdentifierAction = 'CREATE' | 'DISABLE';
export interface MasterDataPageMeta {
page: number;
size: number;
totalElements: number;
totalPages: number;
}
interface ApiEnvelope<T> {
data: T;
meta?: MasterDataPageMeta;
requestId: string;
}
export interface MasterDataReference {
publicId: string;
code?: string | null;
name: string;
}
export interface MasterDataReferences {
companies: MasterDataReference[];
projects: MasterDataReference[];
counterparties: MasterDataReference[];
costCategories: MasterDataReference[];
}
export interface MasterDataBase {
publicId: string;
status: string;
version: number;
createdAt: string;
updatedAt: string;
allowedActions: MasterDataAction[];
}
export type MasterDataSnapshot = Record<string, unknown>;
export interface MasterDataVersion {
auditId: string;
version: number;
status: string;
actionCode: string;
actorName: string;
changedAt: string;
before: MasterDataSnapshot | null;
after: MasterDataSnapshot | null;
}
export interface LegacyIdentifier {
publicId: string;
resource: LegacyIdentifierResource;
sourceSystem: string;
legacyCode: string;
targetPublicId: string;
status: 'ACTIVE' | 'DISABLED';
version: number;
createdAt: string;
updatedAt: string;
allowedActions: LegacyIdentifierAction[];
}
export interface LegacyIdentifierResolution {
mappingPublicId: string;
resource: LegacyIdentifierResource;
sourceSystem: string;
legacyCode: string;
targetPublicId: string;
}
export interface CompanyMasterData extends MasterDataBase {
businessNo: string;
name: string;
taxNo?: string | null;
}
export interface CounterpartyMasterData extends MasterDataBase {
businessNo: string;
type: string;
name: string;
taxNo?: string | null;
contactName?: string | null;
contactPhone?: string | null;
}
export interface BankAccountMasterData extends MasterDataBase {
ownerType: string;
owner: MasterDataReference;
accountCategory: string;
accountName: string;
bankName: string;
maskedAccountNo: string;
versionNo: number;
validFrom: string;
validTo?: string | null;
}
export interface ContractMasterData extends MasterDataBase {
company: MasterDataReference;
project: MasterDataReference;
counterparty: MasterDataReference;
businessNo: string;
name: string;
originalAmount: string;
approvedChangeAmount: string;
settlementAmount: string;
currency: string;
}
export interface DictionaryMasterData extends MasterDataBase {
resource: 'departments' | 'cost-categories' | 'accounts' | 'tax-rates';
code: string;
name: string;
parent?: MasterDataReference | null;
accountType?: string | null;
auxiliaryRequired?: boolean | null;
rate?: string | null;
}
export type MasterDataItem =
CompanyMasterData | CounterpartyMasterData | BankAccountMasterData | ContractMasterData | DictionaryMasterData;
function normalizeMasterDataItem(item: MasterDataItem): MasterDataItem {
return {
...item,
allowedActions: Array.isArray(item?.allowedActions) ? item.allowedActions : [],
} as MasterDataItem;
}
function normalizeReferences(references: MasterDataReferences): MasterDataReferences {
return {
companies: Array.isArray(references?.companies) ? references.companies : [],
projects: Array.isArray(references?.projects) ? references.projects : [],
counterparties: Array.isArray(references?.counterparties) ? references.counterparties : [],
costCategories: Array.isArray(references?.costCategories) ? references.costCategories : [],
};
}
export interface MasterDataQuery {
keyword?: string;
status?: string;
type?: string;
page: number;
size: number;
}
export interface MasterDataVersionQuery {
page: number;
size: number;
}
export interface LegacyIdentifierQuery {
keyword?: string;
status?: 'ACTIVE' | 'DISABLED';
page: number;
size: number;
}
export interface LegacyIdentifierPageMeta extends MasterDataPageMeta {
allowedActions: LegacyIdentifierAction[];
}
export interface LegacyIdentifierCreatePayload {
sourceSystem: string;
legacyCode: string;
}
export interface CompanyCreatePayload {
businessNo: string;
name: string;
taxNo?: string;
}
export interface CompanyUpdatePayload {
name: string;
taxNo?: string;
version: number;
}
export interface CounterpartyCreatePayload {
businessNo: string;
type: string;
name: string;
taxNo?: string;
contactName?: string;
contactPhone?: string;
}
export interface CounterpartyUpdatePayload extends Omit<CounterpartyCreatePayload, 'businessNo'> {
version: number;
}
export interface BankAccountCreatePayload {
ownerType: string;
ownerId: string;
accountCategory: string;
accountName: string;
bankName: string;
accountNo: string;
validFrom: string;
validTo?: string;
}
export interface BankAccountUpdatePayload {
accountCategory: string;
accountName: string;
bankName: string;
accountNo?: string;
validFrom: string;
validTo?: string;
version: number;
}
export interface ContractCreatePayload {
companyId: string;
projectId: string;
counterpartyId: string;
businessNo: string;
name: string;
originalAmount: string;
currency: string;
}
export interface ContractUpdatePayload extends Omit<ContractCreatePayload, 'businessNo'> {
version: number;
}
export interface DictionaryCreatePayload {
code: string;
name: string;
parentId?: string;
accountType?: string;
auxiliaryRequired?: boolean;
rate?: string;
}
export interface DictionaryUpdatePayload extends Omit<DictionaryCreatePayload, 'code'> {
version: number;
}
export type MasterDataCreatePayload =
| CompanyCreatePayload
| CounterpartyCreatePayload
| BankAccountCreatePayload
| ContractCreatePayload
| DictionaryCreatePayload;
export type MasterDataUpdatePayload =
| CompanyUpdatePayload
| CounterpartyUpdatePayload
| BankAccountUpdatePayload
| ContractUpdatePayload
| DictionaryUpdatePayload;
export interface MasterDataReviewPayload {
version: number;
decision: 'APPROVE' | 'RETURN';
opinion: string;
}
export async function getMasterDataPage(resource: MasterDataResource, params: MasterDataQuery) {
const envelope = await request.get<ApiEnvelope<MasterDataItem[]>>(
{ url: `/masterdata/${resource}`, params },
{ isTransformResponse: false },
);
return {
items: Array.isArray(envelope?.data) ? envelope.data.map(normalizeMasterDataItem) : [],
meta: envelope.meta || { page: params.page, size: params.size, totalElements: 0, totalPages: 0 },
requestId: envelope.requestId,
};
}
export function getMasterDataDetail(resource: MasterDataResource, publicId: string) {
return request.get<MasterDataItem>({ url: `/masterdata/${resource}/${publicId}` }).then(normalizeMasterDataItem);
}
export async function getMasterDataVersions(
resource: MasterDataResource,
publicId: string,
params: MasterDataVersionQuery,
) {
const envelope = await request.get<ApiEnvelope<MasterDataVersion[]>>(
{ url: `/masterdata/${resource}/${publicId}/versions`, params },
{ isTransformResponse: false },
);
return {
items: Array.isArray(envelope?.data) ? envelope.data : [],
meta: envelope.meta || { page: params.page, size: params.size, totalElements: 0, totalPages: 0 },
requestId: envelope.requestId,
};
}
export async function getLegacyIdentifiers(
resource: LegacyIdentifierResource,
publicId: string,
params: LegacyIdentifierQuery,
) {
const envelope = await request.get<ApiEnvelope<LegacyIdentifier[]>>(
{ url: `/masterdata/${resource}/${publicId}/legacy-identifiers`, params },
{ isTransformResponse: false },
);
const meta = envelope.meta as LegacyIdentifierPageMeta | undefined;
return {
items: Array.isArray(envelope?.data)
? envelope.data.map((item) => ({
...item,
allowedActions: Array.isArray(item.allowedActions) ? item.allowedActions : [],
}))
: [],
meta: {
page: meta?.page || params.page,
size: meta?.size || params.size,
totalElements: meta?.totalElements || 0,
totalPages: meta?.totalPages || 0,
allowedActions: Array.isArray(meta?.allowedActions) ? meta.allowedActions : [],
} as LegacyIdentifierPageMeta,
requestId: envelope.requestId,
};
}
export function createLegacyIdentifier(
resource: LegacyIdentifierResource,
publicId: string,
payload: LegacyIdentifierCreatePayload,
) {
return request.post<LegacyIdentifier>({
url: `/masterdata/${resource}/${publicId}/legacy-identifiers`,
data: payload,
});
}
export function disableLegacyIdentifier(
resource: LegacyIdentifierResource,
targetPublicId: string,
mappingPublicId: string,
version: number,
) {
return request.post<LegacyIdentifier>({
url: `/masterdata/${resource}/${targetPublicId}/legacy-identifiers/${mappingPublicId}/disable`,
data: { version },
});
}
export function resolveLegacyIdentifier(resource: LegacyIdentifierResource, sourceSystem: string, legacyCode: string) {
return request.get<LegacyIdentifierResolution>({
url: `/masterdata/${resource}/legacy-identifiers/resolve`,
params: { sourceSystem, legacyCode },
});
}
export function getMasterDataReferences() {
return request.get<MasterDataReferences>({ url: '/masterdata/references' }).then(normalizeReferences);
}
export function createMasterData(resource: MasterDataResource, payload: MasterDataCreatePayload) {
return request.post<MasterDataItem>({ url: `/masterdata/${resource}`, data: payload }).then(normalizeMasterDataItem);
}
export function updateMasterData(resource: MasterDataResource, publicId: string, payload: MasterDataUpdatePayload) {
return request
.patch<MasterDataItem>({ url: `/masterdata/${resource}/${publicId}`, data: payload })
.then(normalizeMasterDataItem);
}
export function submitMasterData(resource: MasterDataResource, publicId: string, version: number) {
return request
.post<MasterDataItem>({ url: `/masterdata/${resource}/${publicId}/submit`, data: { version } })
.then(normalizeMasterDataItem);
}
export function reviewMasterData(resource: MasterDataResource, publicId: string, payload: MasterDataReviewPayload) {
return request
.post<MasterDataItem>({ url: `/masterdata/${resource}/${publicId}/review`, data: payload })
.then(normalizeMasterDataItem);
}
export function disableMasterData(resource: MasterDataResource, publicId: string, version: number) {
return request
.post<MasterDataItem>({ url: `/masterdata/${resource}/${publicId}/disable`, data: { version } })
.then(normalizeMasterDataItem);
}
+554
View File
@@ -0,0 +1,554 @@
import type { AxiosResponse } from 'axios';
import { request } from '@/utils/request';
import { commandHeaders } from './command';
export type PaymentTab = 'all' | 'my' | 'finance-check' | 'offline' | 'result' | 'paid' | 'failed';
export type PaymentStatus =
| 'DRAFT'
| 'REVIEWING'
| 'RETURNED'
| 'REJECTED'
| 'APPROVED'
| 'OFFLINE_PENDING'
| 'RESULT_RECORDED'
| 'RESULT_VERIFIED'
| 'PART_PAID'
| 'PAID'
| 'FAILED'
| 'REFUNDED'
| 'VOID';
export type PaymentAction =
| 'EDIT'
| 'SUBMIT'
| 'VOID'
| 'APPROVE'
| 'RETURN'
| 'REQUEST_CONTROL_EXCEPTION'
| 'RECORD_SUCCESS'
| 'RECORD_FAILURE'
| 'RECORD_REFUND'
| 'VERIFY_RESULT'
| 'RETURN_RESULT'
| 'CREATE_REPAYMENT';
export interface PageMeta {
page: number;
size: number;
totalElements: number;
totalPages: number;
}
export interface Reference {
publicId: string;
code?: string | null;
name: string;
}
export interface ContractReference extends Reference {
companyId: string;
projectId: string;
supplierId: string;
}
export interface BankAccountReference {
publicId: string;
supplierId: string;
accountName: string;
bankName: string;
maskedAccountNo: string;
versionNo: number;
}
export interface PayableReference {
publicId: string;
businessNo: string;
projectId: string;
contractId: string;
supplierId: string;
costCategoryCode?: string | null;
costCategoryName?: string | null;
amount: string;
paidAmount: string;
remainingAmount: string;
}
export interface PaymentAttachmentRequirement {
documentType: string;
name: string;
required: boolean;
allowNotApplicable: boolean;
}
export interface PaymentAttachmentRule {
parameterId: string;
versionNo: number;
costCategoryCode: string;
costCategoryName: string;
requirements: PaymentAttachmentRequirement[];
}
export interface PaymentReferenceOptions {
companies: Reference[];
projects: Reference[];
suppliers: Reference[];
contracts: ContractReference[];
payables: PayableReference[];
bankAccounts: BankAccountReference[];
attachmentRules: PaymentAttachmentRule[];
}
export interface FundControl {
projectId: string;
projectCode: string;
projectName: string;
currency: string;
confirmedReceipt: string;
confirmedDeduction: string;
approvedAdjustment: string;
frozenAmount: string;
approvedUnpaid: string;
paidAmount: string;
availableBalance: string;
version: number;
}
export interface FundLedgerEntry {
publicId: string;
entryType: string;
sourceType: string;
sourcePublicId?: string | null;
sourceBusinessNo?: string | null;
description: string;
currency: string;
eventAmount: string;
deltaAmount: string;
confirmedReceiptDelta: string;
frozenAmountDelta: string;
paidAmountDelta: string;
balanceAfter: string;
controlVersionAfter: number;
createdByName?: string | null;
occurredAt: string;
}
export interface FundLedgerPage {
control: FundControl;
items: FundLedgerEntry[];
meta: PageMeta;
}
export interface FundLedgerQuery {
projectId: string;
currency?: string;
entryType?: string;
dateFrom?: string;
dateTo?: string;
page: number;
size: number;
}
export interface PaymentQuery {
tab: PaymentTab;
companyId?: string;
projectId?: string;
supplierId?: string;
status?: PaymentStatus;
keyword?: string;
riskCode?: string;
submittedDateFrom?: string;
submittedDateTo?: string;
sort: string;
page: number;
size: number;
}
export interface Payment {
publicId: string;
businessNo: string;
company: Reference;
project: Reference;
contract: Reference;
payable?: Reference | null;
supplier: Reference;
bankAccount: BankAccountReference;
sourceDocumentId?: string | null;
originalPaymentId?: string | null;
repaymentSequence: number;
requestedAmount: string;
approvedAmount?: string | null;
frozenAmount?: string | null;
paidAmount?: string | null;
currency: string;
requestedDate: string;
purpose: string;
status: PaymentStatus;
submittedByName?: string | null;
approvedByName?: string | null;
resultRecordedByName?: string | null;
resultVerifiedByName?: string | null;
version: number;
createdAt: string;
updatedAt: string;
allowedActions: PaymentAction[];
}
export type PaymentCheckCode =
| 'PROJECT_FUNDS'
| 'CONTRACT_LIMIT'
| 'PAYABLE_LIMIT'
| 'ACCOUNT_ACTIVE'
| 'ATTACHMENTS'
| 'DUPLICATE'
| 'SOURCE_APPROVED'
| 'PROJECT_RISK';
export interface PaymentCheck {
publicId: string;
code: PaymentCheckCode;
result: 'PASS' | 'WARNING' | 'BLOCK';
message: string;
checkedAt: string;
}
export interface PaymentResult {
publicId: string;
resultNo: string;
resultType: 'SUCCESS' | 'FAILURE' | 'REFUND';
resultAt: string;
amount?: string | null;
failureReason?: string | null;
remark?: string | null;
recordedByName?: string | null;
recordedAt: string;
verifiedByName?: string | null;
verifiedAt?: string | null;
verificationOpinion?: string | null;
recordedFromStatus?: PaymentStatus | null;
returnedByName?: string | null;
returnedAt?: string | null;
returnOpinion?: string | null;
status: 'PENDING_VERIFY' | 'VERIFIED' | 'RETURNED';
version: number;
receiptFileIds: string[];
}
export interface PaymentAttachment {
publicId: string;
fileId: string;
fileName: string;
documentType: string;
scanStatus: string;
createdAt: string;
}
export interface PaymentAttachmentException {
publicId: string;
documentType: string;
documentName: string;
version: number;
reason: string;
status: 'PENDING' | 'APPROVED' | 'REJECTED';
requestedByName?: string | null;
requestedAt: string;
reviewedByName?: string | null;
reviewedAt?: string | null;
reviewOpinion?: string | null;
consumedAt?: string | null;
allowedActions: Array<'REVIEW'>;
}
export type PaymentControlCode = 'DUPLICATE' | 'PROJECT_RISK';
export interface PaymentControlException {
publicId: string;
controlCode: PaymentControlCode;
findingSummary: string;
version: number;
reason: string;
validUntil: string;
status: 'PENDING' | 'APPROVED' | 'REJECTED';
requestedByName?: string | null;
requestedAt: string;
reviewedByName?: string | null;
reviewedAt?: string | null;
reviewOpinion?: string | null;
consumedAt?: string | null;
allowedActions: Array<'REVIEW'>;
}
export interface PaymentDetail {
payment: Payment;
checks: PaymentCheck[];
results: PaymentResult[];
attachments: PaymentAttachment[];
attachmentExceptions: PaymentAttachmentException[];
controlExceptions: PaymentControlException[];
}
export interface PaymentControlExceptionRequestPayload {
paymentVersion: number;
controlCode: PaymentControlCode;
reason: string;
validUntil: string;
}
export interface PaymentControlExceptionReviewPayload {
paymentVersion: number;
version: number;
decision: 'APPROVE' | 'REJECT';
opinion: string;
}
export interface PaymentDraftPayload {
companyId: string;
projectId: string;
contractId: string;
payableId?: string;
supplierId: string;
bankAccountVersionId: string;
sourceDocumentId?: string;
requestedAmount: string;
currency: string;
requestedDate: string;
purpose: string;
attachments: Array<{ fileId: string; documentType: string }>;
attachmentExceptions: Array<{ documentType: string; reason: string }>;
}
export interface PaymentUpdatePayload extends PaymentDraftPayload {
version: number;
}
export interface PaymentResultPayload {
version: number;
resultAt: string;
amount: string;
receiptFileIds: string[];
remark?: string;
}
export interface PaymentFailurePayload {
version: number;
resultAt: string;
reason: string;
receiptFileIds?: string[];
remark?: string;
}
export interface PaymentRefundPayload extends PaymentResultPayload {
reason: string;
}
export interface RepaymentPayload {
version: number;
requestedDate: string;
requestedAmount: string;
purpose: string;
}
interface PageEnvelope<T> {
data: T[];
meta: PageMeta;
requestId: string;
}
async function page<T>(url: string, params: PaymentQuery) {
const envelope = await request.get<PageEnvelope<T>>({ url, params }, { isTransformResponse: false });
return {
items: Array.isArray(envelope?.data) ? envelope.data : [],
meta: envelope?.meta || { page: params.page, size: params.size, totalElements: 0, totalPages: 0 },
requestId: envelope?.requestId || '',
};
}
export function getPaymentReferences() {
return request.get<PaymentReferenceOptions>({ url: '/payments/references' });
}
function normalizePayment(payment: Payment): Payment {
return { ...payment, allowedActions: Array.isArray(payment?.allowedActions) ? payment.allowedActions : [] };
}
export async function getPayments(params: PaymentQuery) {
const result = await page<Payment>('/payments', params);
return { ...result, items: result.items.map(normalizePayment) };
}
export async function getPayment(publicId: string) {
const detail = await request.get<PaymentDetail>({ url: `/payments/${publicId}` });
return {
...detail,
payment: normalizePayment(detail.payment),
checks: Array.isArray(detail?.checks) ? detail.checks : [],
results: (Array.isArray(detail?.results) ? detail.results : []).map((item) => ({
...item,
receiptFileIds: Array.isArray(item?.receiptFileIds) ? item.receiptFileIds : [],
})),
attachments: Array.isArray(detail?.attachments) ? detail.attachments : [],
attachmentExceptions: (Array.isArray(detail?.attachmentExceptions) ? detail.attachmentExceptions : []).map(
(item) => ({
...item,
allowedActions: Array.isArray(item?.allowedActions) ? item.allowedActions : [],
}),
),
controlExceptions: (Array.isArray(detail?.controlExceptions) ? detail.controlExceptions : []).map((item) => ({
...item,
allowedActions: Array.isArray(item?.allowedActions) ? item.allowedActions : [],
})),
};
}
export function getPaymentChecks(publicId: string) {
return request.get<PaymentCheck[]>({ url: `/payments/${publicId}/checks` });
}
export function getFundLedger(params: FundLedgerQuery) {
return request.get<FundLedgerPage>({ url: '/funds/ledger', params });
}
export function createPayment(payload: PaymentDraftPayload, idempotencyKey?: string) {
return request.post<Payment>({
url: '/payments',
data: payload,
...commandHeaders('payment-create', idempotencyKey),
});
}
export function updatePayment(publicId: string, payload: PaymentUpdatePayload, idempotencyKey?: string) {
return request.patch<Payment>({
url: `/payments/${publicId}`,
data: payload,
...commandHeaders('payment-update', idempotencyKey),
});
}
export function reviewPaymentAttachmentException(
paymentPublicId: string,
exceptionPublicId: string,
version: number,
decision: 'APPROVE' | 'REJECT',
opinion: string,
idempotencyKey?: string,
) {
return request.post<Payment>({
url: `/payments/${paymentPublicId}/attachment-exceptions/${exceptionPublicId}/review`,
data: { version, decision, opinion },
...commandHeaders('payment-attachment-exception-review', idempotencyKey),
});
}
export function requestPaymentControlException(
paymentPublicId: string,
payload: PaymentControlExceptionRequestPayload,
idempotencyKey?: string,
) {
return request.post<Payment>({
url: `/payments/${paymentPublicId}/control-exceptions`,
data: payload,
...commandHeaders('payment-control-exception-request', idempotencyKey),
});
}
export function reviewPaymentControlException(
paymentPublicId: string,
exceptionPublicId: string,
payload: PaymentControlExceptionReviewPayload,
idempotencyKey?: string,
) {
return request.post<Payment>({
url: `/payments/${paymentPublicId}/control-exceptions/${exceptionPublicId}/review`,
data: payload,
...commandHeaders('payment-control-exception-review', idempotencyKey),
});
}
export function submitPayment(publicId: string, version: number, idempotencyKey?: string) {
return request.post<Payment>({
url: `/payments/${publicId}/submit`,
data: { version },
...commandHeaders('payment-submit', idempotencyKey),
});
}
export function approvePayment(publicId: string, version: number, idempotencyKey?: string) {
return request.post<Payment>({
url: `/payments/${publicId}/approve`,
data: { version },
...commandHeaders('payment-approve', idempotencyKey),
});
}
export function returnPayment(publicId: string, version: number, opinion: string, idempotencyKey?: string) {
return request.post<Payment>({
url: `/payments/${publicId}/return`,
data: { version, opinion },
...commandHeaders('payment-return', idempotencyKey),
});
}
export function voidPayment(publicId: string, version: number, opinion: string, idempotencyKey?: string) {
return request.post<Payment>({
url: `/payments/${publicId}/void`,
data: { version, opinion },
...commandHeaders('payment-void', idempotencyKey),
});
}
export function confirmOfflinePaymentResult(publicId: string, payload: PaymentResultPayload, idempotencyKey?: string) {
return request.post<Payment>({
url: `/payments/${publicId}/confirm-offline-result`,
data: payload,
...commandHeaders('payment-result', idempotencyKey),
});
}
export function recordPaymentFailure(publicId: string, payload: PaymentFailurePayload, idempotencyKey?: string) {
return request.post<Payment>({
url: `/payments/${publicId}/record-failure`,
data: payload,
...commandHeaders('payment-failure', idempotencyKey),
});
}
export function recordPaymentRefund(publicId: string, payload: PaymentRefundPayload, idempotencyKey?: string) {
return request.post<Payment>({
url: `/payments/${publicId}/record-refund`,
data: payload,
...commandHeaders('payment-refund', idempotencyKey),
});
}
export function verifyPaymentResult(publicId: string, version: number, opinion: string, idempotencyKey?: string) {
return request.post<Payment>({
url: `/payments/${publicId}/verify-result`,
data: { version, opinion },
...commandHeaders('payment-verify', idempotencyKey),
});
}
export function returnPaymentResult(publicId: string, version: number, opinion: string, idempotencyKey?: string) {
return request.post<Payment>({
url: `/payments/${publicId}/return-result`,
data: { version, opinion },
...commandHeaders('payment-return-result', idempotencyKey),
});
}
export function createRepayment(publicId: string, payload: RepaymentPayload, idempotencyKey?: string) {
return request.post<Payment>({
url: `/payments/${publicId}/create-repayment`,
data: payload,
...commandHeaders('payment-repayment', idempotencyKey),
});
}
export function exportPayments(params: Omit<PaymentQuery, 'page' | 'size'>) {
return request.post<AxiosResponse<Blob>>(
{ url: '/payments/exports', data: params, responseType: 'blob' },
{ isReturnNativeResponse: true },
);
}
+350
View File
@@ -0,0 +1,350 @@
import { request } from '@/utils/request';
import { commandHeaders } from './command';
export interface ApiPageMeta {
page: number;
size: number;
totalElements: number;
totalPages: number;
}
interface ApiEnvelope<T> {
data: T;
meta?: ApiPageMeta;
requestId: string;
}
export interface ProjectReference {
publicId: string;
name: string;
}
export interface ProjectReferenceOptions {
companies: ProjectReference[];
customers: ProjectReference[];
owners: ProjectReference[];
}
export interface ProjectOverview {
contractAmount: string;
receivedAmount: string;
paidAmount: string;
availableBalance: string;
sourceFormCount: number;
contractCount: number;
receiptCount: number;
invoiceCount: number;
paymentCount: number;
progressCount: number;
peopleCount: number;
accountingCount: number;
voucherCount: number;
pendingTaskCount: number;
fileCount: number;
activeRiskCount: number;
documentCompleteness: number;
}
export interface ProjectRiskFlag {
publicId: string;
riskType: string;
reason: string;
effectiveFrom: string;
effectiveUntil?: string | null;
blocking: boolean;
status: string;
version: number;
createdByName: string;
createdAt: string;
resolvedReason?: string | null;
resolvedByName?: string | null;
resolvedAt?: string | null;
allowedActions: string[];
}
export interface ProjectTimelineItem {
publicId: string;
actionCode: string;
resultCode: string;
reason?: string | null;
username: string;
activeRole?: string | null;
occurredAt: string;
}
export interface ProjectListItem {
publicId: string;
businessNo: string;
name: string;
company: ProjectReference;
customer?: ProjectReference | null;
owner: ProjectReference;
contractAmount: string;
receivedAmount: string;
paidAmount: string;
currency: string;
stage: string;
financeStatus: string;
archiveStatus: string;
status: string;
pendingTaskCount: number;
documentCompleteness: number;
startDate?: string | null;
endDate?: string | null;
version: number;
updatedAt: string;
allowedActions: string[];
}
export interface ProjectDetail extends Omit<
ProjectListItem,
'contractAmount' | 'receivedAmount' | 'paidAmount' | 'pendingTaskCount' | 'documentCompleteness'
> {
description?: string | null;
createdAt: string;
overview: ProjectOverview;
riskFlags: ProjectRiskFlag[];
timeline: ProjectTimelineItem[];
}
export interface ProjectQuery {
companyId?: string;
keyword?: string;
ownerId?: string;
customerId?: string;
stage?: string;
financeStatus?: string;
archiveStatus?: string;
status?: string;
startDateFrom?: string;
startDateTo?: string;
endDateFrom?: string;
endDateTo?: string;
sort?: string;
page: number;
size: number;
}
export interface ProjectExport {
exportId: string;
rowCount: number;
sha256: string;
filterHash: string;
columns: string[];
fileName: string;
content: string;
}
export type ProjectExportPayload = Omit<ProjectQuery, 'page' | 'size'> & { columns?: string[] };
export interface ProjectImportRow {
rowNo: number;
businessNo: string;
name: string;
description?: string;
companyId: string;
customerId?: string;
ownerId: string;
startDate?: string;
endDate?: string;
currency: string;
}
export interface ProjectImportIssue {
rowNo: number;
fieldCode?: string | null;
message: string;
}
export interface ProjectImportSummary {
publicId: string;
batchNo: string;
sourceFileId: string;
fileSha256: string;
totalRows: number;
validRows: number;
errorRows: number;
status: string;
version: number;
createdAt: string;
updatedAt: string;
issues: ProjectImportIssue[];
}
export interface ProjectImportPayload {
sourceFileId: string;
fileSha256: string;
/** Compatibility field for pre-server-parser clients; new clients omit it. */
rows?: ProjectImportRow[];
}
export interface ProjectCreatePayload {
businessNo: string;
name: string;
description?: string;
companyId: string;
customerId?: string;
ownerId: string;
startDate?: string;
endDate?: string;
currency: string;
}
export interface ProjectUpdatePayload {
name: string;
description?: string;
customerId?: string;
ownerId: string;
startDate?: string;
endDate?: string;
version: number;
}
export interface ProjectRiskFlagCreatePayload {
riskType: string;
reason: string;
effectiveFrom?: string;
effectiveUntil?: string;
blocking: boolean;
}
export type ProjectSubresource =
| 'forms'
| 'contracts'
| 'receipts'
| 'invoices'
| 'payments'
| 'progress'
| 'people'
| 'accounting'
| 'archives'
| 'timeline'
| 'risk-flags';
export interface ProjectSubresourceQuery {
page: number;
size: number;
[key: string]: string | number | boolean | undefined;
}
export interface ProjectSubresourceEnvelope<T> {
data: T;
meta?: ApiPageMeta;
requestId: string;
}
/**
* Read-only PAGE-08 drill-down query. The resource is a closed union so a
* caller cannot turn a project URL into an arbitrary backend path.
*/
export function getProjectSubresource<T>(
projectId: string,
resource: ProjectSubresource,
params: ProjectSubresourceQuery,
) {
return request
.get<ProjectSubresourceEnvelope<T>>(
{ url: `/projects/${projectId}/${resource}`, params },
{ isTransformResponse: false },
)
.then((envelope) => ({
...envelope,
data: envelope && envelope.data !== undefined ? envelope.data : ([] as T),
meta: envelope?.meta || { page: params.page, size: params.size, totalElements: 0, totalPages: 0 },
}));
}
export async function getProjects(params: ProjectQuery) {
const envelope = await request.get<ApiEnvelope<ProjectListItem[]>>(
{ url: '/projects', params },
{ isTransformResponse: false },
);
return {
items: Array.isArray(envelope?.data) ? envelope.data : [],
meta: envelope.meta || { page: params.page, size: params.size, totalElements: 0, totalPages: 0 },
requestId: envelope.requestId,
};
}
export function exportProjects(payload: ProjectExportPayload) {
return request.post<ProjectExport>({ url: '/projects/exports', data: payload });
}
export function createProjectImport(payload: ProjectImportPayload, idempotencyKey: string) {
return request.post<ProjectImportSummary>({
url: '/projects/imports',
data: payload,
headers: { 'Idempotency-Key': idempotencyKey },
});
}
export function getProjectImport(publicId: string) {
return request.get<ProjectImportSummary>({ url: `/projects/imports/${publicId}` });
}
export function confirmProjectImport(publicId: string, version: number, idempotencyKey: string) {
return request.post<ProjectDetail[]>({
url: `/projects/imports/${publicId}/confirm`,
data: { version },
headers: { 'Idempotency-Key': idempotencyKey },
});
}
export function getProject(publicId: string) {
return request.get<ProjectDetail>({ url: `/projects/${publicId}` });
}
export function getProjectReferences() {
return request.get<ProjectReferenceOptions>({ url: '/projects/references' });
}
export function createProject(payload: ProjectCreatePayload, idempotencyKey?: string) {
return request.post<ProjectDetail>({
url: '/projects',
data: payload,
...commandHeaders('project-create', idempotencyKey),
});
}
export function updateProject(publicId: string, payload: ProjectUpdatePayload, idempotencyKey?: string) {
return request.patch<ProjectDetail>({
url: `/projects/${publicId}`,
data: payload,
...commandHeaders('project-update', idempotencyKey),
});
}
export function completeProjectFinance(publicId: string, version: number, idempotencyKey?: string) {
return request.post<ProjectDetail>({
url: `/projects/${publicId}/finance-complete`,
data: { version },
...commandHeaders('project-finance-complete', idempotencyKey),
});
}
export function createProjectRiskFlag(
publicId: string,
payload: ProjectRiskFlagCreatePayload,
idempotencyKey?: string,
) {
return request.post<ProjectRiskFlag>({
url: `/projects/${publicId}/risk-flags`,
data: payload,
...commandHeaders('project-risk-create', idempotencyKey),
});
}
export function resolveProjectRiskFlag(
publicId: string,
riskPublicId: string,
version: number,
reason: string,
idempotencyKey?: string,
) {
return request.post<ProjectRiskFlag>({
url: `/projects/${publicId}/risk-flags/${riskPublicId}/resolve`,
data: { version, reason },
...commandHeaders('project-risk-resolve', idempotencyKey),
});
}
+320
View File
@@ -0,0 +1,320 @@
import type { AxiosResponse } from 'axios';
import { request } from '@/utils/request';
import { commandHeaders } from './command';
export interface PageMeta {
page: number;
size: number;
totalElements: number;
totalPages: number;
}
export interface Reference {
publicId: string;
code?: string | null;
name: string;
}
export interface ReferenceOptions {
companies: Reference[];
projects: Reference[];
counterparties: Reference[];
contracts: Reference[];
receivables: ReceivableOption[];
}
export interface ReceivableOption {
publicId: string;
businessNo: string;
project: Reference;
counterparty: Reference;
amount: string;
outstandingAmount: string;
currency: string;
}
export interface ReceivableQuery {
companyId?: string;
projectId?: string;
counterpartyId?: string;
keyword?: string;
status?: string;
dateFrom?: string;
dateTo?: string;
sort?: string;
page: number;
size: number;
}
export interface Receipt {
publicId: string;
businessNo: string;
company: Reference;
project?: Reference | null;
customer?: Reference | null;
receiptDate: string;
amount: string;
allocatedAmount: string;
unallocatedAmount: string;
currency: string;
payerName: string;
referenceNo?: string | null;
status: string;
confirmedByName?: string | null;
confirmedAt?: string | null;
voidReason?: string | null;
version: number;
createdAt: string;
updatedAt: string;
allowedActions: string[];
}
export interface ReceiptAllocation {
publicId: string;
sequenceNo: number;
receivable?: Reference | null;
project: Reference;
contract?: Reference | null;
amount: string;
receivableAmount?: string | null;
receivableOutstandingAmount?: string | null;
note?: string | null;
createdByName?: string | null;
createdAt: string;
}
export interface Receivable {
publicId: string;
businessNo: string;
company: Reference;
project: Reference;
contract?: Reference | null;
counterparty: Reference;
businessDate: string;
amount: string;
settledAmount: string;
outstandingAmount: string;
currency: string;
status: string;
sourceInvoiceId?: string | null;
version: number;
createdAt: string;
updatedAt: string;
}
export interface ReceiptDetail {
receipt: Receipt;
allocations: ReceiptAllocation[];
}
export interface Invoice {
publicId: string;
businessNo: string;
sourceDocumentId?: string | null;
sourceVersionId?: string | null;
company: Reference;
project: Reference;
contract?: Reference | null;
counterparty: Reference;
invoiceType: string;
amount: string;
currency: string;
taxRate: string;
requestedDate: string;
invoiceNo?: string | null;
issuedDate?: string | null;
resultFileId?: string | null;
originalInvoiceId?: string | null;
status: string;
submittedByName?: string | null;
reviewedByName?: string | null;
resultRecordedByName?: string | null;
submittedAt?: string | null;
reviewedAt?: string | null;
reviewOpinion?: string | null;
resultRecordedAt?: string | null;
terminationReason?: string | null;
contractLimit?: string | null;
contractCommittedAmount?: string | null;
contractAvailableAmount?: string | null;
version: number;
createdAt: string;
updatedAt: string;
allowedActions: string[];
}
interface PageEnvelope<T> {
data: T[];
meta: PageMeta;
requestId: string;
}
interface DataEnvelope<T> {
data: T;
requestId: string;
}
async function page<T>(url: string, params: ReceivableQuery) {
const envelope = await request.get<PageEnvelope<T>>({ url, params }, { isTransformResponse: false });
return {
items: Array.isArray(envelope?.data) ? envelope.data : [],
meta: envelope?.meta || { page: params.page, size: params.size, totalElements: 0, totalPages: 0 },
requestId: envelope?.requestId || '',
};
}
function normalizeReceipt(receipt: Receipt): Receipt {
return { ...receipt, allowedActions: Array.isArray(receipt?.allowedActions) ? receipt.allowedActions : [] };
}
function normalizeInvoice(invoice: Invoice): Invoice {
return { ...invoice, allowedActions: Array.isArray(invoice?.allowedActions) ? invoice.allowedActions : [] };
}
function normalizeReceiptDetail(detail: ReceiptDetail): ReceiptDetail {
return {
...detail,
receipt: normalizeReceipt(detail.receipt),
allocations: Array.isArray(detail?.allocations) ? detail.allocations : [],
};
}
export function getReceivableReferences() {
return request.get<ReferenceOptions>({ url: '/receivables/references' });
}
export async function getReceipts(params: ReceivableQuery) {
const result = await page<Receipt>('/receipts', params);
return { ...result, items: result.items.map(normalizeReceipt) };
}
export function getReceivables(params: ReceivableQuery) {
return page<Receivable>('/receivables', params);
}
export function getReceipt(publicId: string) {
return request.get<ReceiptDetail>({ url: `/receipts/${publicId}` }).then(normalizeReceiptDetail);
}
export function createReceipt(payload: Record<string, unknown>, idempotencyKey?: string) {
return request.post<Receipt>({
url: '/receipts',
data: payload,
...commandHeaders('receipt-create', idempotencyKey),
});
}
export function updateReceipt(publicId: string, payload: Record<string, unknown>, idempotencyKey?: string) {
return request.patch<Receipt>({
url: `/receipts/${publicId}`,
data: payload,
...commandHeaders('receipt-update', idempotencyKey),
});
}
export function confirmReceipt(publicId: string, version: number, idempotencyKey?: string) {
return request.post<Receipt>({
url: `/receipts/${publicId}/confirm`,
data: { version },
...commandHeaders('receipt-confirm', idempotencyKey),
});
}
export function allocateReceipt(publicId: string, payload: Record<string, unknown>, idempotencyKey?: string) {
return request.post<ReceiptDetail>({
url: `/receipts/${publicId}/allocations`,
data: payload,
...commandHeaders('receipt-allocate', idempotencyKey),
});
}
export function voidReceipt(publicId: string, payload: Record<string, unknown>, idempotencyKey?: string) {
return request.post<Receipt>({
url: `/receipts/${publicId}/void`,
data: payload,
...commandHeaders('receipt-void', idempotencyKey),
});
}
export function exportReceipts(params: Omit<ReceivableQuery, 'page' | 'size'>) {
return request.get<AxiosResponse<Blob>>(
{ url: '/receipts/export', params, responseType: 'blob' },
{ isReturnNativeResponse: true },
);
}
export async function getInvoices(params: ReceivableQuery) {
const result = await page<Invoice>('/invoices', params);
return { ...result, items: result.items.map(normalizeInvoice) };
}
export function getInvoice(publicId: string) {
return request.get<Invoice>({ url: `/invoices/${publicId}` }).then(normalizeInvoice);
}
export function createInvoice(payload: Record<string, unknown>, idempotencyKey?: string) {
return request.post<Invoice>({
url: '/invoices',
data: payload,
...commandHeaders('invoice-create', idempotencyKey),
});
}
export function updateInvoice(publicId: string, payload: Record<string, unknown>, idempotencyKey?: string) {
return request.patch<Invoice>({
url: `/invoices/${publicId}`,
data: payload,
...commandHeaders('invoice-update', idempotencyKey),
});
}
export function submitInvoice(publicId: string, version: number, idempotencyKey?: string) {
return request.post<Invoice>({
url: `/invoices/${publicId}/submit`,
data: { version },
...commandHeaders('invoice-submit', idempotencyKey),
});
}
export function reviewInvoice(publicId: string, payload: Record<string, unknown>, idempotencyKey?: string) {
return request.post<Invoice>({
url: `/invoices/${publicId}/review`,
data: payload,
...commandHeaders('invoice-review', idempotencyKey),
});
}
export function recordInvoiceResult(publicId: string, payload: Record<string, unknown>, idempotencyKey?: string) {
return request.post<Invoice>({
url: `/invoices/${publicId}/record-result`,
data: payload,
...commandHeaders('invoice-result', idempotencyKey),
});
}
export function voidInvoice(publicId: string, payload: Record<string, unknown>, idempotencyKey?: string) {
return request.post<Invoice>({
url: `/invoices/${publicId}/void`,
data: payload,
...commandHeaders('invoice-void', idempotencyKey),
});
}
export function redLetterInvoice(publicId: string, payload: Record<string, unknown>, idempotencyKey?: string) {
return request.post<Invoice>({
url: `/invoices/${publicId}/red-letter`,
data: payload,
...commandHeaders('invoice-red-letter', idempotencyKey),
});
}
export function exportInvoices(params: Omit<ReceivableQuery, 'page' | 'size'>) {
return request.get<AxiosResponse<Blob>>(
{ url: '/invoices/export', params, responseType: 'blob' },
{ isReturnNativeResponse: true },
);
}
export type ReceivableDataEnvelope<T> = DataEnvelope<T>;
+191
View File
@@ -0,0 +1,191 @@
import { request } from '@/utils/request';
export type ReportCode =
| 'project-ledger'
| 'contract-execution'
| 'receipt-invoice'
| 'payment-progress'
| 'workflow-duration'
| 'archive-completeness';
export interface ReportPageMeta {
page: number;
size: number;
totalElements: number;
totalPages: number;
reportCode: ReportCode;
definitionVersion: string;
filterHash: string;
columns: string[];
sort: string;
summary: Record<string, string>;
allowedActions: string[];
}
export interface ReportQuery {
companyId?: string;
projectId?: string;
counterpartyId?: string;
keyword?: string;
status?: string;
dateFrom?: string;
dateTo?: string;
page: number;
size: number;
}
export type ReportRow = Record<string, unknown> & { rowId?: string; publicId?: string };
export interface ReportDrilldown {
reportCode: ReportCode;
rowId: string;
definitionVersion: string;
filterHash: string;
source: Record<string, unknown>;
groups: Array<{ code: string; label: string; value: string }>;
relatedRecords: Array<{
relationType: string;
objectType: string;
publicId: string;
businessNo: string;
status: string;
amount: string;
businessDate: string;
sourceDocumentId: string;
}>;
sourceForms: Array<{
publicId: string;
businessNo: string;
formType: string;
sourceSystem: string;
sourceNo: string;
status: string;
sourceVersionId: string;
versionNo?: number;
relationType: string;
ownerType: string;
ownerPublicId: string;
exactVersion: boolean;
}>;
approvals: Array<{
publicId: string;
sourceDocumentId: string;
sourceVersionId: string;
nodeCode: string;
action: string;
actorName: string;
opinion: string;
actedAt: string;
}>;
attachments: Array<{
fileId: string;
ownerType: string;
ownerId: string;
sourceDocumentId: string;
documentType: string;
displayName: string;
scanStatus: string;
versionNo?: number;
}>;
}
export interface ReportExport {
exportId: string;
reportCode: ReportCode;
definitionVersion: string;
filterHash: string;
status: string;
estimatedRows: number;
rowCount: number;
sha256: string;
fileName: string;
content: string;
}
export interface ReportExportPreview {
exportId: string;
reportCode: ReportCode;
definitionVersion: string;
filterHash: string;
status: 'PREPARED';
estimatedRows: number;
columns: string[];
sort: string;
expiresAt: string;
maskingNotice: string;
}
export interface ReportExportSnapshot {
reportCode: ReportCode;
filters: Omit<ReportQuery, 'page' | 'size'>;
definitionVersion: string;
filterHash: string;
}
function requiredString(value: unknown, field: string): string {
if (typeof value !== 'string' || !value.trim()) throw new Error(`报表导出响应缺少${field}`);
return value;
}
function nonNegativeInteger(value: unknown, field: string): number {
if (!Number.isInteger(value) || Number(value) < 0) throw new Error(`报表导出响应中的${field}无效`);
return Number(value);
}
export function normalizeReportExport(value: ReportExport): ReportExport {
return {
exportId: requiredString(value?.exportId, '导出编号'),
reportCode: requiredString(value?.reportCode, '报表编号') as ReportCode,
definitionVersion: requiredString(value?.definitionVersion, '口径版本'),
filterHash: requiredString(value?.filterHash, '筛选快照'),
status: requiredString(value?.status, '导出状态'),
estimatedRows: nonNegativeInteger(value?.estimatedRows, '预计行数'),
rowCount: nonNegativeInteger(value?.rowCount, '导出行数'),
sha256: requiredString(value?.sha256, '文件摘要'),
fileName: requiredString(value?.fileName, '文件名'),
content: requiredString(value?.content, '文件内容'),
};
}
interface ReportEnvelope {
data: ReportRow[];
meta: ReportPageMeta;
requestId: string;
}
export async function getReport(reportCode: ReportCode, params: ReportQuery) {
const envelope = await request.get<ReportEnvelope>(
{ url: `/reports/${reportCode}`, params },
{ isTransformResponse: false },
);
return {
items: envelope.data || [],
meta: envelope.meta,
requestId: envelope.requestId,
};
}
export function getReportDrilldown(reportCode: ReportCode, rowId: string, params: Omit<ReportQuery, 'page' | 'size'>) {
return request.get<ReportDrilldown>({ url: `/reports/${reportCode}/${rowId}/drilldown`, params });
}
export function prepareReportExport(
reportCode: ReportCode,
payload: Omit<ReportQuery, 'page' | 'size'> & {
columns?: string[];
sort: string;
expectedFilterHash: string;
expectedDefinitionVersion: string;
},
) {
return request.post<ReportExportPreview>({ url: `/reports/${reportCode}/exports/prepare`, data: payload });
}
export function confirmReportExport(reportCode: ReportCode, exportId: string) {
return request
.post<ReportExport>({
url: `/reports/${reportCode}/exports/confirm`,
data: { exportId, maskingConfirmed: true },
})
.then(normalizeReportExport);
}
+61
View File
@@ -0,0 +1,61 @@
import { request } from '@/utils/request';
export interface SetupStatus {
required: boolean;
locked: boolean;
supportedDatabaseTypes: string[];
message: string;
}
export interface SetupDatabaseRequest {
setupCode: string;
databaseType: 'MYSQL' | 'POSTGRESQL';
host: string;
port: number;
database: string;
username: string;
password: string;
}
export interface SetupConnection {
successful: boolean;
databaseType: string;
serverVersion: string;
schemaReady: boolean;
message: string;
}
export interface SetupCompleteRequest extends SetupDatabaseRequest {
adminUsername: string;
adminDisplayName: string;
adminPassword: string;
adminPasswordConfirmation: string;
}
export interface SetupCompleted {
completed: boolean;
username: string;
message: string;
restartAfterSeconds: number;
}
function normalizeStatus(value: SetupStatus): SetupStatus {
return {
required: Boolean(value?.required),
locked: Boolean(value?.locked),
supportedDatabaseTypes: Array.isArray(value?.supportedDatabaseTypes) ? value.supportedDatabaseTypes : [],
message: value?.message || '',
};
}
export function getSetupStatus() {
return request.get<SetupStatus>({ url: '/setup/status' }).then(normalizeStatus);
}
export function testSetupConnection(data: SetupDatabaseRequest) {
return request.post<SetupConnection>({ url: '/setup/test-connection', data }).then((value) => value);
}
export function completeSetup(data: SetupCompleteRequest) {
return request.post<SetupCompleted>({ url: '/setup/complete', data }).then((value) => value);
}
+564
View File
@@ -0,0 +1,564 @@
import type { UploadFile } from 'tdesign-vue-next';
import { request } from '@/utils/request';
import { commandHeaders } from './command';
export interface ApiPageMeta {
page: number;
size: number;
totalElements: number;
totalPages: number;
}
interface ApiEnvelope<T> {
data: T;
meta?: ApiPageMeta;
requestId: string;
}
export interface ReferenceItem {
publicId: string;
name: string;
}
export interface ContractReferenceItem extends ReferenceItem {
businessNo: string;
companyId: string;
projectId: string;
counterpartyId: string;
counterpartyName: string;
counterpartyType: 'CUSTOMER' | 'SUPPLIER' | 'BOTH';
}
export interface FormReferenceOptions {
companies: ReferenceItem[];
projects: ReferenceItem[];
suppliers: ReferenceItem[];
customers: ReferenceItem[];
contracts: ContractReferenceItem[];
payables: PayableReferenceItem[];
costCategories: ReferenceItem[];
}
export interface PayableReferenceItem {
publicId: string;
businessNo: string;
projectId: string;
contractId: string;
supplierId: string;
amount: string;
paidAmount: string;
remainingAmount: string;
}
export interface TemplateColumnOption {
code: string;
label: string;
}
export interface TemplateColumn {
code: string;
label: string;
dataType?: 'TEXT' | 'LONG_TEXT' | 'ENUM' | 'DATE' | 'DECIMAL' | 'INTEGER' | 'BOOLEAN' | 'REFERENCE' | 'FILE';
required?: boolean;
maxLength?: number;
options?: TemplateColumnOption[];
}
export interface SourceTemplateField {
code: string;
label: string;
sectionCode: string;
sectionName: string;
dataType:
| 'TEXT'
| 'LONG_TEXT'
| 'INTEGER'
| 'DECIMAL'
| 'DATE'
| 'DATETIME'
| 'BOOLEAN'
| 'ENUM'
| 'REFERENCE'
| 'FILE'
| 'TABLE';
required: boolean;
repeatable: boolean;
maxLength?: number;
numericPrecision?: number;
numericScale?: number;
options: TemplateColumn[];
unit?: string;
sensitivity: 'NORMAL' | 'INTERNAL' | 'SENSITIVE' | 'RESTRICTED';
maskingRule?: string;
helpText?: string;
sortOrder: number;
}
export interface SourceTemplate {
publicId: string;
formType: string;
templateVersion: number;
name: string;
groupCode: string;
groupName: string;
purpose: string;
initiatorRoles: string[];
status: string;
publishedAt: string;
myDraftCount: number;
myPendingCount: number;
canCreate: boolean;
fields: SourceTemplateField[];
}
export interface SourceFormSummary {
publicId: string;
businessNo: string;
formType: string;
formName: string;
templateVersion: number;
sourceSystem: string;
sourceNo: string;
sourceVersion: string;
company: ReferenceItem;
project?: ReferenceItem | null;
applicantName: string;
businessDate: string;
status: string;
currentNodeCode?: string | null;
currentNodeName?: string | null;
version: number;
createdByName: string;
createdAt: string;
updatedAt: string;
allowedActions: string[];
}
export interface ValidationIssue {
level: string;
sectionCode: string;
fieldCode: string;
rowNo?: number | null;
message: string;
}
export interface SourceValidation {
documentId: string;
version: number;
valid: boolean;
canSubmit: boolean;
errors: ValidationIssue[];
warnings: ValidationIssue[];
validatedAt: string;
}
export interface ApprovalItem {
publicId: string;
nodeCode: string;
nodeName: string;
actorName: string;
action: string;
opinion?: string;
actedAt: string;
}
export interface SourceFormDetail {
summary: SourceFormSummary;
documentVersion: number;
snapshotSha256: string;
values: Record<string, unknown>;
fields: SourceTemplateField[];
approvals: ApprovalItem[];
validation: SourceValidation;
}
export interface SourceFormPayload {
formType: string;
templateVersion: number;
sourceSystem: string;
sourceNo: string;
sourceVersion: string;
companyId: string;
projectId?: string;
applicantName: string;
businessDate: string;
values: Record<string, unknown>;
}
export interface SourceFormUpdatePayload extends Omit<
SourceFormPayload,
'formType' | 'templateVersion' | 'sourceSystem'
> {
version: number;
}
export interface SourceFormQuery {
groupCode?: string;
formType?: string;
templateVersion?: number;
projectId?: string;
status?: string;
sourceSystem?: string;
createdByMe?: boolean;
keyword?: string;
createdDateFrom?: string;
createdDateTo?: string;
sort?:
| 'updatedAt,asc'
| 'updatedAt,desc'
| 'createdAt,asc'
| 'createdAt,desc'
| 'businessNo,asc'
| 'businessNo,desc'
| 'status,asc'
| 'status,desc';
page: number;
size: number;
}
export interface WorkflowTaskSummary {
publicId: string;
instancePublicId: string;
nodeCode: string;
nodeName: string;
sequenceNo: number;
status: string;
version: number;
assigneeName: string;
arrivedAt: string;
dueAt?: string | null;
updatedAt: string;
keyAmount?: string | null;
form: SourceFormSummary;
allowedActions: string[];
}
export interface WorkflowTaskDetail {
task: WorkflowTaskSummary;
form: SourceFormDetail;
timeline: ApprovalItem[];
}
export interface WorkflowTaskQuery {
view: 'TODO' | 'PROCESSED' | 'CREATED' | 'RETURNED' | 'COMPLETED';
formType?: string;
status?: string;
keyword?: string;
projectId?: string;
arrivedDateFrom?: string;
arrivedDateTo?: string;
overdue?: boolean;
sort?: 'arrivedAt,asc' | 'arrivedAt,desc' | 'dueAt,asc' | 'dueAt,desc' | 'updatedAt,asc' | 'updatedAt,desc';
page: number;
size: number;
}
export interface SourceImportRowPayload {
rowNo: number;
sourceNo: string;
sourceVersion: string;
companyId: string;
projectId?: string;
applicantName: string;
businessDate: string;
values: Record<string, unknown>;
}
export interface SourceImportRequest {
formType: string;
templateVersion: number;
sourceSystem: 'EXCEL_IMPORT' | 'OA_EXPORT';
sourceFileId: string;
fileSha256: string;
rows: SourceImportRowPayload[];
}
export interface SourceImportIssue {
rowNo: number;
fieldCode?: string;
message: string;
}
export interface SourceImportSummary {
publicId: string;
batchNo: string;
formType: string;
sourceSystem: string;
sourceFileId: string;
fileSha256: string;
totalRows: number;
validRows: number;
errorRows: number;
status: 'VALIDATING' | 'INVALID' | 'READY' | 'CONFIRMED' | 'CANCELLED' | string;
version: number;
createdAt: string;
updatedAt: string;
issues: SourceImportIssue[];
}
export interface FileView {
publicId: string;
originalName: string;
mediaType: string;
extension: string;
sizeBytes: number;
sha256: string;
scanStatus: string;
createdAt: string;
}
export function getSourceTemplates() {
return request
.get<SourceTemplate[]>({ url: '/source/templates' })
.then((items) => (Array.isArray(items) ? items : []).map(normalizeTemplate));
}
export function getSourceTemplate(formType: string, version: number) {
return request
.get<SourceTemplate>({ url: `/source/templates/${formType}/versions/${version}` })
.then(normalizeTemplate);
}
export function getSourceReferences() {
return request.get<FormReferenceOptions>({ url: '/source/references' }).then((references) => ({
companies: Array.isArray(references?.companies) ? references.companies : [],
projects: Array.isArray(references?.projects) ? references.projects : [],
suppliers: Array.isArray(references?.suppliers) ? references.suppliers : [],
customers: Array.isArray(references?.customers) ? references.customers : [],
contracts: Array.isArray(references?.contracts) ? references.contracts : [],
payables: Array.isArray(references?.payables) ? references.payables : [],
costCategories: Array.isArray(references?.costCategories) ? references.costCategories : [],
}));
}
export async function getSourceForms(params: SourceFormQuery) {
const envelope = await request.get<ApiEnvelope<SourceFormSummary[]>>(
{ url: '/source/forms', params },
{ isTransformResponse: false },
);
return {
items: (Array.isArray(envelope?.data) ? envelope.data : []).map(normalizeSummary),
meta: envelope.meta || { page: params.page, size: params.size, totalElements: 0, totalPages: 0 },
requestId: envelope.requestId,
};
}
export function getSourceForm(publicId: string) {
return request.get<SourceFormDetail>({ url: `/source/forms/${publicId}` }).then(normalizeDetail);
}
export function createSourceForm(payload: SourceFormPayload, idempotencyKey?: string) {
return request
.post<SourceFormDetail>({
url: '/source/forms',
data: payload,
...commandHeaders('source-form-create', idempotencyKey),
})
.then(normalizeDetail);
}
export function updateSourceForm(publicId: string, payload: SourceFormUpdatePayload, idempotencyKey?: string) {
return request
.patch<SourceFormDetail>({
url: `/source/forms/${publicId}`,
data: payload,
...commandHeaders('source-form-update', idempotencyKey),
})
.then(normalizeDetail);
}
export function validateSourceForm(publicId: string, idempotencyKey?: string) {
return request
.post<SourceValidation>({
url: `/source/forms/${publicId}/validate`,
data: {},
...commandHeaders('source-form-validate', idempotencyKey),
})
.then(normalizeValidation);
}
export function submitSourceForm(publicId: string, version: number, idempotencyKey?: string) {
return request
.post<SourceFormDetail>({
url: `/source/forms/${publicId}/submit`,
data: { version },
...commandHeaders('source-form-submit', idempotencyKey),
})
.then(normalizeDetail);
}
export function withdrawSourceForm(publicId: string, version: number, opinion: string, idempotencyKey?: string) {
return request
.post<SourceFormDetail>({
url: `/source/forms/${publicId}/withdraw`,
data: { version, opinion },
...commandHeaders('source-form-withdraw', idempotencyKey),
})
.then(normalizeDetail);
}
export function voidSourceForm(publicId: string, version: number, opinion: string, idempotencyKey?: string) {
return request
.post<SourceFormDetail>({
url: `/source/forms/${publicId}/void`,
data: { version, opinion },
...commandHeaders('source-form-void', idempotencyKey),
})
.then(normalizeDetail);
}
export async function getWorkflowTasks(params: WorkflowTaskQuery) {
const envelope = await request.get<ApiEnvelope<WorkflowTaskSummary[]>>(
{ url: '/workflow/tasks', params },
{ isTransformResponse: false },
);
return {
items: (Array.isArray(envelope?.data) ? envelope.data : []).map(normalizeTask),
meta: envelope.meta || { page: params.page, size: params.size, totalElements: 0, totalPages: 0 },
requestId: envelope.requestId,
};
}
export function getWorkflowTask(publicId: string) {
return request.get<WorkflowTaskDetail>({ url: `/workflow/tasks/${publicId}` }).then(normalizeTaskDetail);
}
export function actOnWorkflowTask(
publicId: string,
action: 'approve' | 'return' | 'reject',
payload: { version: number; opinion: string; returnPoints: string[] },
idempotencyKey?: string,
) {
return request
.post<WorkflowTaskDetail>({
url: `/workflow/tasks/${publicId}/${action}`,
data: payload,
...commandHeaders(`workflow-task-${action}`, idempotencyKey),
})
.then(normalizeTaskDetail);
}
export function withdrawWorkflowInstance(
instancePublicId: string,
version: number,
opinion: string,
idempotencyKey?: string,
) {
return request
.post<SourceFormDetail>({
url: `/workflow/instances/${instancePublicId}/withdraw`,
data: { version, opinion },
...commandHeaders('workflow-instance-withdraw', idempotencyKey),
})
.then(normalizeDetail);
}
export function voidWorkflowInstance(
instancePublicId: string,
version: number,
opinion: string,
idempotencyKey?: string,
) {
return request
.post<SourceFormDetail>({
url: `/workflow/instances/${instancePublicId}/void`,
data: { version, opinion },
...commandHeaders('workflow-instance-void', idempotencyKey),
})
.then(normalizeDetail);
}
export function createSourceImport(payload: SourceImportRequest, idempotencyKey?: string) {
return request.post<SourceImportSummary>({
url: '/source/imports',
data: payload,
...commandHeaders('source-import-create', idempotencyKey),
});
}
export function getSourceImport(publicId: string) {
return request.get<SourceImportSummary>({ url: `/source/imports/${publicId}` });
}
export function confirmSourceImport(publicId: string, version: number, idempotencyKey?: string) {
return request
.post<SourceFormDetail[]>({
url: `/source/imports/${publicId}/confirm`,
data: { version },
...commandHeaders('source-import-confirm', idempotencyKey),
})
.then((items) => (Array.isArray(items) ? items : []).map(normalizeDetail));
}
export async function uploadBusinessFile(uploadFile: UploadFile) {
if (!uploadFile.raw) throw new Error('未读取到待上传文件');
const formData = new FormData();
formData.append('file', uploadFile.raw);
return request.post<FileView>({
url: '/files',
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
...commandHeaders('business-file-upload').headers,
},
});
}
function normalizeTemplate(template: SourceTemplate): SourceTemplate {
return {
...template,
initiatorRoles: Array.isArray(template?.initiatorRoles) ? template.initiatorRoles : [],
fields: (Array.isArray(template?.fields) ? template.fields : []).map((field) => ({
...field,
options: Array.isArray(field.options) ? field.options : [],
})),
};
}
function normalizeSummary(summary: SourceFormSummary): SourceFormSummary {
return {
...summary,
allowedActions: Array.isArray(summary?.allowedActions) ? summary.allowedActions : [],
};
}
function normalizeValidation(validation: SourceValidation): SourceValidation {
return {
...validation,
errors: Array.isArray(validation?.errors) ? validation.errors : [],
warnings: Array.isArray(validation?.warnings) ? validation.warnings : [],
};
}
function normalizeDetail(detail: SourceFormDetail): SourceFormDetail {
return {
...detail,
summary: normalizeSummary(detail.summary),
values: detail?.values && typeof detail.values === 'object' ? detail.values : {},
fields: (Array.isArray(detail?.fields) ? detail.fields : []).map((field) => ({
...field,
options: Array.isArray(field.options) ? field.options : [],
})),
approvals: Array.isArray(detail?.approvals) ? detail.approvals : [],
validation: normalizeValidation(detail.validation),
};
}
function normalizeTask(task: WorkflowTaskSummary): WorkflowTaskSummary {
return {
...task,
form: normalizeSummary(task.form),
allowedActions: Array.isArray(task?.allowedActions) ? task.allowedActions : [],
};
}
function normalizeTaskDetail(detail: WorkflowTaskDetail): WorkflowTaskDetail {
return {
...detail,
task: normalizeTask(detail.task),
form: normalizeDetail(detail.form),
timeline: Array.isArray(detail?.timeline) ? detail.timeline : [],
};
}
+56
View File
@@ -0,0 +1,56 @@
import { request } from '@/utils/request';
import { commandHeaders } from './command';
export interface SystemUpdateView {
enabled: boolean;
currentVersion: string;
latestVersion?: string | null;
updateAvailable: boolean;
state: string;
message: string;
releaseNotes?: string | null;
publishedAt?: string | null;
checkedAt?: string | null;
statusUpdatedAt?: string | null;
allowedActions: string[];
}
function normalize(value: SystemUpdateView): SystemUpdateView {
return {
...value,
enabled: Boolean(value?.enabled),
updateAvailable: Boolean(value?.updateAvailable),
allowedActions: Array.isArray(value?.allowedActions) ? value.allowedActions : [],
};
}
export function getSystemUpdateStatus() {
return request.get<SystemUpdateView>({ url: '/admin/system-update' }).then(normalize);
}
export function checkSystemUpdate() {
return request
.post<SystemUpdateView>({ url: '/admin/system-update/check', ...commandHeaders('system-update-check') })
.then(normalize);
}
export function downloadSystemUpdate(version: string) {
return request
.post<SystemUpdateView>({
url: '/admin/system-update/download',
data: { version },
...commandHeaders('system-update-download'),
})
.then(normalize);
}
export function installSystemUpdate(version: string, reason?: string) {
return request
.post<SystemUpdateView>({
url: '/admin/system-update/install',
data: { version, reason },
...commandHeaders('system-update-install'),
})
.then(normalize);
}
+113
View File
@@ -0,0 +1,113 @@
import { request } from '@/utils/request';
export interface WorkbenchMetric {
code: string;
label: string;
value: number;
targetRoute: string;
}
export interface WorkbenchTodo {
publicId: string;
formType: string;
formName: string;
businessNo: string;
projectPublicId?: string;
projectName?: string;
nodeCode: string;
status: string;
arrivedAt?: string;
dueAt?: string;
targetRoute: string;
}
export interface WorkbenchProject {
publicId: string;
businessNo: string;
name: string;
companyName: string;
stage: string;
status: string;
contractAmount: string;
updatedAt?: string;
targetRoute: string;
}
export interface WorkbenchIssue {
code: string;
label: string;
severity: string;
count: number;
detail: string;
targetRoute: string;
}
export interface WorkbenchFund {
projectPublicId: string;
projectName: string;
currency: string;
confirmedReceipt: string;
approvedUnpaid: string;
paidAmount: string;
frozenAmount: string;
availableAmount: string;
targetRoute: string;
}
export interface WorkbenchArchiveItem {
kind: string;
publicId: string;
title: string;
subtitle: string;
status: string;
completeness?: string;
missingCount: number;
dueAt?: string;
updatedAt?: string;
targetRoute: string;
}
export interface WorkbenchActivity {
publicId: string;
actionCode: string;
objectType: string;
objectPublicId?: string;
title: string;
resultCode: string;
occurredAt?: string;
targetRoute: string;
}
export interface WorkbenchView {
roleCode: string;
title: string;
refreshedAt: string;
metrics: WorkbenchMetric[];
todos: WorkbenchTodo[];
projects: WorkbenchProject[];
issues: WorkbenchIssue[];
funds: WorkbenchFund[];
archiveItems: WorkbenchArchiveItem[];
activities: WorkbenchActivity[];
}
const array = <T>(value: T[] | null | undefined): T[] => (Array.isArray(value) ? value : []);
export function normalizeWorkbench(view: Partial<WorkbenchView> | null | undefined): WorkbenchView {
return {
roleCode: String(view?.roleCode || ''),
title: String(view?.title || ''),
refreshedAt: String(view?.refreshedAt || ''),
metrics: array(view?.metrics),
todos: array(view?.todos),
projects: array(view?.projects),
issues: array(view?.issues),
funds: array(view?.funds),
archiveItems: array(view?.archiveItems),
activities: array(view?.activities),
};
}
export function getWorkbench(role: 'project' | 'finance' | 'archive') {
return request.get<WorkbenchView>({ url: `/workbenches/${role}` }).then(normalizeWorkbench);
}