feat(claimFilesMetadata) - Added feature for having claims uploaded files metadata

This commit is contained in:
2025-09-17 00:29:58 +05:30
parent cb7123afc5
commit d0a984a7e9
6 changed files with 125 additions and 6 deletions

View File

@@ -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) => ({

View File

@@ -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 },
}); });
}, },

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}; };