feat(claimFilesMetadata) - Added feature for having claims uploaded files metadata
This commit is contained in:
@@ -246,6 +246,18 @@ router.get("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
// Create a new claim
|
// Create a new claim
|
||||||
router.post("/", async (req: Request, res: Response): Promise<any> => {
|
router.post("/", async (req: Request, res: Response): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
|
// --- TRANSFORM claimFiles (if provided) into Prisma nested-create shape
|
||||||
|
if (Array.isArray(req.body.claimFiles)) {
|
||||||
|
// each item expected: { filename: string, mimeType: string }
|
||||||
|
req.body.claimFiles = {
|
||||||
|
create: req.body.claimFiles.map((f: any) => ({
|
||||||
|
filename: String(f.filename),
|
||||||
|
mimeType: String(f.mimeType || f.mime || ""),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TRANSFORM serviceLines
|
||||||
if (Array.isArray(req.body.serviceLines)) {
|
if (Array.isArray(req.body.serviceLines)) {
|
||||||
req.body.serviceLines = req.body.serviceLines.map(
|
req.body.serviceLines = req.body.serviceLines.map(
|
||||||
(line: InputServiceLine) => ({
|
(line: InputServiceLine) => ({
|
||||||
|
|||||||
@@ -578,6 +578,7 @@ export const storage: IStorage = {
|
|||||||
include: {
|
include: {
|
||||||
serviceLines: true,
|
serviceLines: true,
|
||||||
staff: true,
|
staff: true,
|
||||||
|
claimFiles: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -600,7 +601,7 @@ export const storage: IStorage = {
|
|||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
skip: offset,
|
skip: offset,
|
||||||
take: limit,
|
take: limit,
|
||||||
include: { serviceLines: true, staff: true },
|
include: { serviceLines: true, staff: true, claimFiles: true },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ import {
|
|||||||
} from "@/utils/procedureCombosMapping";
|
} from "@/utils/procedureCombosMapping";
|
||||||
import { COMBO_CATEGORIES, PROCEDURE_COMBOS } from "@/utils/procedureCombos";
|
import { COMBO_CATEGORIES, PROCEDURE_COMBOS } from "@/utils/procedureCombos";
|
||||||
|
|
||||||
|
interface ClaimFileMeta {
|
||||||
|
filename: string;
|
||||||
|
mimeType: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface ClaimFormData {
|
interface ClaimFormData {
|
||||||
patientId: number;
|
patientId: number;
|
||||||
appointmentId: number;
|
appointmentId: number;
|
||||||
@@ -63,6 +68,7 @@ interface ClaimFormData {
|
|||||||
status: string; // default "pending"
|
status: string; // default "pending"
|
||||||
serviceLines: InputServiceLine[];
|
serviceLines: InputServiceLine[];
|
||||||
claimId?: number;
|
claimId?: number;
|
||||||
|
claimFiles?: ClaimFileMeta[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClaimFormProps {
|
interface ClaimFormProps {
|
||||||
@@ -341,6 +347,13 @@ export function ClaimForm({
|
|||||||
(line) => line.procedureCode.trim() !== ""
|
(line) => line.procedureCode.trim() !== ""
|
||||||
);
|
);
|
||||||
const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = form;
|
const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = form;
|
||||||
|
|
||||||
|
// build claimFiles metadata from uploadedFiles (only filename + mimeType)
|
||||||
|
const claimFilesMeta: ClaimFileMeta[] = (uploadedFiles || []).map((f) => ({
|
||||||
|
filename: f.name,
|
||||||
|
mimeType: f.type,
|
||||||
|
}));
|
||||||
|
|
||||||
const createdClaim = await onSubmit({
|
const createdClaim = await onSubmit({
|
||||||
...formToCreateClaim,
|
...formToCreateClaim,
|
||||||
serviceLines: filteredServiceLines,
|
serviceLines: filteredServiceLines,
|
||||||
@@ -348,6 +361,7 @@ export function ClaimForm({
|
|||||||
patientId: patientId,
|
patientId: patientId,
|
||||||
insuranceProvider: "MassHealth",
|
insuranceProvider: "MassHealth",
|
||||||
appointmentId: appointmentId!,
|
appointmentId: appointmentId!,
|
||||||
|
claimFiles: claimFilesMeta,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. sending form data to selenium service
|
// 4. sending form data to selenium service
|
||||||
@@ -415,6 +429,13 @@ export function ClaimForm({
|
|||||||
(line) => line.procedureCode.trim() !== ""
|
(line) => line.procedureCode.trim() !== ""
|
||||||
);
|
);
|
||||||
const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = form;
|
const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = form;
|
||||||
|
|
||||||
|
// build claimFiles metadata from uploadedFiles (only filename + mimeType)
|
||||||
|
const claimFilesMeta: ClaimFileMeta[] = (uploadedFiles || []).map((f) => ({
|
||||||
|
filename: f.name,
|
||||||
|
mimeType: f.type,
|
||||||
|
}));
|
||||||
|
|
||||||
const createdClaim = await onSubmit({
|
const createdClaim = await onSubmit({
|
||||||
...formToCreateClaim,
|
...formToCreateClaim,
|
||||||
serviceLines: filteredServiceLines,
|
serviceLines: filteredServiceLines,
|
||||||
@@ -422,6 +443,7 @@ export function ClaimForm({
|
|||||||
patientId: patientId,
|
patientId: patientId,
|
||||||
insuranceProvider: "MassHealth",
|
insuranceProvider: "MassHealth",
|
||||||
appointmentId: appointmentId!,
|
appointmentId: appointmentId!,
|
||||||
|
claimFiles: claimFilesMeta,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Close form
|
// 4. Close form
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { formatDateToHumanReadable } from "@/utils/dateUtils";
|
import { formatDateToHumanReadable } from "@/utils/dateUtils";
|
||||||
import { ClaimWithServiceLines } from "@repo/db/types";
|
import { ClaimFileMeta, ClaimWithServiceLines } from "@repo/db/types";
|
||||||
|
import { FileText, Paperclip } from "lucide-react";
|
||||||
|
|
||||||
type ClaimViewModalProps = {
|
type ClaimViewModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -25,6 +26,40 @@ export default function ClaimViewModal({
|
|||||||
claim,
|
claim,
|
||||||
onEditClaim,
|
onEditClaim,
|
||||||
}: ClaimViewModalProps) {
|
}: ClaimViewModalProps) {
|
||||||
|
// Normalizer: supports both ClaimFile[] and nested-create shape { create: ClaimFile[] }
|
||||||
|
const getClaimFilesArray = (
|
||||||
|
c: ClaimWithServiceLines | null
|
||||||
|
): ClaimFileMeta[] => {
|
||||||
|
if (!c) return [];
|
||||||
|
|
||||||
|
// If it's already a plain array (runtime from Prisma include), return it
|
||||||
|
const maybeFiles = (c as any).claimFiles;
|
||||||
|
if (!maybeFiles) return [];
|
||||||
|
|
||||||
|
if (Array.isArray(maybeFiles)) {
|
||||||
|
// ensure each item has filename field (best-effort)
|
||||||
|
return maybeFiles.map((f: any) => ({
|
||||||
|
id: f?.id,
|
||||||
|
filename: String(f?.filename ?? ""),
|
||||||
|
mimeType: f?.mimeType ?? f?.mime ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested-create shape: { create: [...] }
|
||||||
|
if (maybeFiles && Array.isArray(maybeFiles.create)) {
|
||||||
|
return maybeFiles.create.map((f: any) => ({
|
||||||
|
id: f?.id,
|
||||||
|
filename: String(f?.filename ?? ""),
|
||||||
|
mimeType: f?.mimeType ?? f?.mime ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// No recognized shape -> empty
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const claimFiles = getClaimFilesArray(claim);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||||
@@ -204,6 +239,37 @@ export default function ClaimViewModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Claim Files (metadata) */}
|
||||||
|
<div className="pt-4">
|
||||||
|
<h4 className="font-medium text-gray-900 flex items-center space-x-2">
|
||||||
|
<Paperclip className="w-4 h-4 inline-block" />
|
||||||
|
<span>Attached Files</span>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{claimFiles.length > 0 ? (
|
||||||
|
<ul className="mt-3 space-y-2">
|
||||||
|
{claimFiles.map((f) => (
|
||||||
|
<li
|
||||||
|
key={f.id ?? f.filename}
|
||||||
|
className="flex items-center justify-between border rounded-md p-3 bg-white"
|
||||||
|
>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<FileText className="w-5 h-5 text-gray-500 mt-1" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{f.filename}</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{f.mimeType || "unknown"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="mt-2 text-gray-500">No files attached.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 pt-4">
|
<div className="flex justify-end space-x-2 pt-4">
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
Close
|
Close
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ model Claim {
|
|||||||
staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id])
|
staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id])
|
||||||
|
|
||||||
serviceLines ServiceLine[]
|
serviceLines ServiceLine[]
|
||||||
|
claimFiles ClaimFile[]
|
||||||
payment Payment?
|
payment Payment?
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +163,15 @@ enum ServiceLineStatus {
|
|||||||
DENIED
|
DENIED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ClaimFile {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
claimId Int
|
||||||
|
filename String
|
||||||
|
mimeType String
|
||||||
|
|
||||||
|
claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
model InsuranceCredential {
|
model InsuranceCredential {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId Int
|
userId Int
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { ClaimStatusSchema, ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
import {
|
||||||
import {z} from "zod";
|
ClaimStatusSchema,
|
||||||
|
ClaimUncheckedCreateInputObjectSchema,
|
||||||
|
} from "@repo/db/usedSchemas";
|
||||||
|
import { z } from "zod";
|
||||||
import { Decimal } from "decimal.js";
|
import { Decimal } from "decimal.js";
|
||||||
import { Staff } from "@repo/db/types";
|
import { Staff } from "@repo/db/types";
|
||||||
|
|
||||||
@@ -34,7 +37,11 @@ export const ExtendedClaimSchema = (
|
|||||||
export type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
|
export type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
|
||||||
export type ClaimStatus = z.infer<typeof ClaimStatusSchema>;
|
export type ClaimStatus = z.infer<typeof ClaimStatusSchema>;
|
||||||
|
|
||||||
|
export type ClaimFileMeta = {
|
||||||
|
id?: number;
|
||||||
|
filename: string;
|
||||||
|
mimeType?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
//used in claim-form
|
//used in claim-form
|
||||||
export interface InputServiceLine {
|
export interface InputServiceLine {
|
||||||
@@ -64,4 +71,5 @@ export type ClaimWithServiceLines = Claim & {
|
|||||||
status: string;
|
status: string;
|
||||||
}[];
|
}[];
|
||||||
staff?: Staff | null;
|
staff?: Staff | null;
|
||||||
|
claimFiles?: ClaimFileMeta[] | null;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user