feat(sp8): Claim drawer Download 837 button
Three pieces:
- src/lib/download.ts: generic downloadTextFile(filename, mime, text)
helper. Mirrors csv.ts:downloadCsv but takes an explicit MIME type and
drops the BOM prepend (which would corrupt the ISA segment).
- src/lib/api.ts: serializeClaim837(id) → {text, filename}. Fetches
GET /api/claims/{id}/serialize-837, pulls the suggested filename from
Content-Disposition (falls back to claim-{id}.x12 if the header is
missing). Throws ApiError on non-2xx so callers can branch on .status.
- ClaimDrawerHeader: Download icon button between the amount and the
close button. Click → api.serializeClaim837 → downloadTextFile.
Disabled + 'Downloading 837 file' aria-label while the fetch is in
flight so the click feels responsive. Optional onError prop surfaces
fetch failures; defaults to a no-op so existing callers stay clean.
Tests: 3 download.test.ts, 3 api.test.ts, 2 header.test.ts (happy
path + error path). Frontend: 350 passing (+8 from 342).
This commit is contained in:
@@ -476,6 +476,42 @@ async function getClaimDetail(id: string): Promise<ClaimDetail> {
|
||||
return (await res.json()) as ClaimDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a claim back to an X12 837P file via the backend's outbound
|
||||
* serializer (SP8).
|
||||
*
|
||||
* Returns both the regenerated X12 text and the filename the backend
|
||||
* suggested (from the `Content-Disposition: attachment; filename=...`
|
||||
* header). Callers usually pipe both into `downloadTextFile` so the user
|
||||
* gets a byte-faithful file named after the claim id.
|
||||
*
|
||||
* Throws `ApiError` on non-2xx — 404 (claim missing) and 422 (stored
|
||||
* `raw_json` unparseable / serializer rejected the payload) are both
|
||||
* reachable, and the caller may want to branch on `.status` to surface a
|
||||
* useful message ("claim data is corrupted — open the batch file
|
||||
* manually" vs. "this claim no longer exists").
|
||||
*/
|
||||
async function serializeClaim837(
|
||||
id: string,
|
||||
): Promise<{ text: string; filename: string }> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(
|
||||
joinUrl(`/api/claims/${encodeURIComponent(id)}/serialize-837`),
|
||||
);
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new ApiError(res.status, detail || res.statusText);
|
||||
}
|
||||
const text = await res.text();
|
||||
// Filename comes from Content-Disposition (set by the backend). Fall back
|
||||
// to a sensible default if the header is missing or malformed so the UI
|
||||
// always has something to hand to `downloadTextFile`.
|
||||
const cd = res.headers.get("content-disposition") ?? "";
|
||||
const match = /filename="?([^";]+)"?/i.exec(cd);
|
||||
const filename = match?.[1] ?? `claim-${id}.x12`;
|
||||
return { text, filename };
|
||||
}
|
||||
|
||||
async function listRemittances<T = unknown>(
|
||||
params: ListRemittancesParams
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
@@ -700,6 +736,7 @@ export const api = {
|
||||
getBatchDiff,
|
||||
listClaims,
|
||||
getClaimDetail,
|
||||
serializeClaim837,
|
||||
listRemittances,
|
||||
getRemittance,
|
||||
listProviders,
|
||||
|
||||
Reference in New Issue
Block a user