文件
hyb-oa/client/src/api/purchaseRequests.ts
T

430 行
11 KiB
TypeScript

import { apiRequestBlob, apiRequestData } from "./http";
import type { AttachmentGovernanceFields } from "../lib/attachmentGovernance";
import type { PrecheckResponse, RiskFlag } from "./feeControl";
export type PurchaseRequestStatus = "draft" | "reviewing" | "approved" | "rejected" | "cancelled";
export type PurchaseRequestSummary = {
id: string;
requestNo: string | null;
applicantId: string;
submitterId: string | null;
departmentId: string;
legalEntityId: string | null;
costCenterId: string | null;
supplierId: string | null;
documentCategoryId: string | null;
budgetPoolId: string | null;
projectId: string | null;
title: string;
reason: string | null;
currencyCode: string;
totalAmount: string;
expectedDeliveryDate: string | null;
status: PurchaseRequestStatus;
createdAt: string | null;
updatedAt: string | null;
applicant: {
id: string;
employeeNo: string;
name: string;
} | null;
submitter: {
id: string;
employeeNo: string;
name: string;
} | null;
department: {
id: string;
name: string;
} | null;
legalEntity: {
id: string;
code: string;
name: string;
} | null;
costCenter: {
id: string;
code: string;
name: string;
legalEntityId: string;
} | null;
supplier: {
id: string;
code: string;
name: string;
} | null;
documentCategory: {
id: string;
code: string;
name: string;
entityType: string;
} | null;
budgetPool: {
id: string;
code: string;
name: string;
} | null;
project: {
id: string;
projectNo: string;
name: string;
status: string;
} | null;
};
export type PurchaseRequestItem = {
id: string;
description: string;
specification: string | null;
quantity: string;
unitPrice: string;
lineAmount: string;
isAssetCandidate: boolean;
createdAt: string | null;
updatedAt: string | null;
};
export type PurchaseAttachment = {
id: string;
originalName: string;
mimeType: string;
fileSize: number;
fileSha256: string | null;
} & AttachmentGovernanceFields & {
uploadedBy: string;
createdAt: string | null;
uploader: {
id: string;
employeeNo: string;
name: string;
} | null;
};
export type PurchaseArchivedFile = {
storagePath: string | null;
originalName: string | null;
mimeType: string | null;
fileSize: number;
};
export type PurchaseOrderArchive = {
id: string;
poNo: string;
contractNo: string | null;
archivedAt: string | null;
archivedBy: string;
archivedByEmployee: {
id: string;
employeeNo: string;
name: string;
} | null;
archivedFiles: PurchaseArchivedFile[];
};
export type PurchaseErpTrackStatus =
| "not_triggered"
| "pending"
| "processing"
| "succeeded"
| "failed"
| "dead_letter"
| "skipped";
export type PurchaseErpBinding = {
id: string;
provider: string;
entityType: string;
localId: string;
externalId: string | null;
externalCode: string | null;
lastSyncedAt: string | null;
};
export type PurchaseErpOutbox = {
id: string;
eventType: string;
status: string;
attempts: number;
lastError: string | null;
processedAt: string | null;
updatedAt: string | null;
};
export type PurchaseErpTrackStage = {
eligible: boolean;
reason: string | null;
status: PurchaseErpTrackStatus | string;
preview: Record<string, unknown>;
outbox: PurchaseErpOutbox | null;
binding?: PurchaseErpBinding | null;
bindings?: {
po: PurchaseErpBinding | null;
contract: PurchaseErpBinding | null;
};
};
export type PurchaseRequestErpTrack = {
purchaseRequest: {
id: string;
requestNo: string | null;
status: string;
totalAmount: string;
supplier: {
id: string;
code: string | null;
name: string | null;
} | null;
archiveOrder: {
id: string;
poNo: string;
contractNo: string | null;
archivedAt: string | null;
} | null;
};
stages: {
commitmentJournal: PurchaseErpTrackStage;
accrualJournal: PurchaseErpTrackStage;
archiveSummary: PurchaseErpTrackStage;
};
permissions: {
canRetry: boolean;
};
};
export type PurchaseRequestErpRequeueTarget = "commitment_journal" | "accrual_journal" | "archive_summary";
export type PurchaseRequestDetail = PurchaseRequestSummary & {
items: PurchaseRequestItem[];
attachments: PurchaseAttachment[];
pendingWorkflow: {
instanceId: string;
currentStatus: string;
currentStepId: string;
task: {
id: string;
stepId: string;
stepName: string;
assigneeId: string | null;
assigneeRole: string | null;
} | null;
} | null;
approvalHistory: Array<{
id: string;
taskId: string | null;
actorId: string;
actorName: string | null;
actorEmployeeNo: string | null;
action: string;
comment: string | null;
createdAt: string | null;
}>;
archiveOrder: PurchaseOrderArchive | null;
permissions: {
canEditDraft: boolean;
canSubmit: boolean;
canWithdraw: boolean;
canArchiveOrder: boolean;
};
riskFlags: RiskFlag[];
};
export type PurchaseRequestListResponse = {
page: number;
pageSize: number;
total: number;
mine: boolean;
items: PurchaseRequestSummary[];
};
export type PurchaseRequestListQuery = {
status?: PurchaseRequestStatus;
page?: number;
pageSize?: number;
mine?: boolean;
supplierId?: string;
legalEntityId?: string;
costCenterId?: string;
projectId?: string;
};
export type PurchaseRequestItemPayload = {
description: string;
specification?: string | null;
quantity: string;
unitPrice: string;
isAssetCandidate?: boolean;
};
function toQueryString(query: Record<string, string | number | boolean | undefined>) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value == null || value === "") {
continue;
}
params.set(key, typeof value === "boolean" ? (value ? "1" : "0") : String(value));
}
const encoded = params.toString();
return encoded ? `?${encoded}` : "";
}
export async function fetchPurchaseRequests(query: PurchaseRequestListQuery = {}) {
return apiRequestData<PurchaseRequestListResponse>(
`/api/purchase-requests${toQueryString(query as Record<string, string | number | boolean | undefined>)}`,
);
}
export async function createPurchaseRequest() {
return apiRequestData<{ purchaseRequest: PurchaseRequestDetail }>("/api/purchase-requests", {
method: "POST",
body: JSON.stringify({}),
});
}
export async function fetchPurchaseRequestDetail(purchaseRequestId: string) {
return apiRequestData<PurchaseRequestDetail>(`/api/purchase-requests/${purchaseRequestId}`);
}
export async function updatePurchaseRequest(
purchaseRequestId: string,
payload: Partial<{
title: string;
reason: string | null;
currencyCode: string;
legalEntityId: string | null;
costCenterId: string | null;
supplierId: string | null;
documentCategoryId: string | null;
budgetPoolId: string | null;
projectId: string | null;
expectedDeliveryDate: string | null;
items: PurchaseRequestItemPayload[];
}>,
) {
return apiRequestData<{ purchaseRequest: PurchaseRequestDetail }>(`/api/purchase-requests/${purchaseRequestId}`, {
method: "PATCH",
body: JSON.stringify(payload),
});
}
export async function precheckPurchaseRequest(purchaseRequestId: string) {
return apiRequestData<PrecheckResponse>(`/api/purchase-requests/${purchaseRequestId}/precheck`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function submitPurchaseRequest(purchaseRequestId: string) {
return apiRequestData<{
purchaseRequestId: string;
requestNo: string;
status: "reviewing";
workflowInstanceId: string;
currentTaskId: string;
budgetPoolId: string | null;
warnings: string[];
budgetReservation: {
applied: boolean;
reservedAmount: string;
};
precheckDecision: "pass" | "warn" | "manual_review";
openedRiskFlagCount: number;
}>(`/api/purchase-requests/${purchaseRequestId}/submit`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function withdrawPurchaseRequest(purchaseRequestId: string) {
return apiRequestData<{
purchaseRequestId: string;
status: "draft";
workflowInstanceId: string | null;
budgetRelease: {
applied: boolean;
items: Array<{ budgetPoolId: string; amount: string }>;
};
}>(`/api/purchase-requests/${purchaseRequestId}/withdraw`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function fetchPurchaseOrderArchive(purchaseRequestId: string) {
return apiRequestData<{ archiveOrder: PurchaseOrderArchive | null }>(`/api/purchase-requests/${purchaseRequestId}/archive-order`);
}
export async function archivePurchaseOrder(
purchaseRequestId: string,
payload: {
poNo: string;
contractNo?: string | null;
archivedFiles?: PurchaseArchivedFile[];
},
) {
return apiRequestData<{ archiveOrder: PurchaseOrderArchive }>(`/api/purchase-requests/${purchaseRequestId}/archive-order`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function uploadPurchaseAttachment(purchaseRequestId: string, file: File) {
const formData = new FormData();
formData.append("file", file);
return apiRequestData<{ attachment: PurchaseAttachment }>(`/api/purchase-requests/${purchaseRequestId}/attachments`, {
method: "POST",
body: formData,
});
}
export async function deletePurchaseAttachment(purchaseRequestId: string, attachmentId: string) {
return apiRequestData<{ outcome: "deleted"; attachmentId: string }>(
`/api/purchase-requests/${purchaseRequestId}/attachments/${attachmentId}`,
{
method: "DELETE",
},
);
}
export async function downloadPurchaseAttachment(purchaseRequestId: string, attachmentId: string, reason?: string) {
return apiRequestBlob(
`/api/purchase-requests/${purchaseRequestId}/attachments/${attachmentId}/download`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(reason ? { reason } : {}),
},
);
}
export async function fetchPurchaseRequestErpTrack(purchaseRequestId: string) {
return apiRequestData<PurchaseRequestErpTrack>(
`/api/integrations/erp/procurement/purchase-requests/${purchaseRequestId}/track`,
);
}
export async function requeuePurchaseRequestErpSync(
purchaseRequestId: string,
payload: {
targets: PurchaseRequestErpRequeueTarget[];
},
runNow = true,
) {
return apiRequestData<{
items: Array<{
target: PurchaseRequestErpRequeueTarget;
outboxId: string;
status: string;
duplicated: boolean;
requeued: boolean;
eventType: string;
}>;
}>(
`/api/integrations/erp/procurement/purchase-requests/${purchaseRequestId}/requeue${toQueryString({ runNow })}`,
{
method: 'POST',
body: JSON.stringify(payload),
},
);
}