diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts index 6bac228..6b93541 100644 --- a/apps/Backend/src/storage/index.ts +++ b/apps/Backend/src/storage/index.ts @@ -146,6 +146,7 @@ type ClaimWithServiceLines = Claim & { toothSurface: string | null; billedAmount: number; }[]; + staff: Staff | null; }; // Pdf types: @@ -538,7 +539,7 @@ export const storage: IStorage = { orderBy: { createdAt: "desc" }, skip: offset, take: limit, - include: { serviceLines: true }, + include: { serviceLines: true, staff: true }, }); }, diff --git a/apps/Frontend/src/components/claims/claim-edit-modal.tsx b/apps/Frontend/src/components/claims/claim-edit-modal.tsx new file mode 100644 index 0000000..2d89850 --- /dev/null +++ b/apps/Frontend/src/components/claims/claim-edit-modal.tsx @@ -0,0 +1,254 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { formatDateToHumanReadable } from "@/utils/dateUtils"; +import React, { useState } from "react"; +import { z } from "zod"; +import { + ClaimUncheckedCreateInputObjectSchema, + StaffUncheckedCreateInputObjectSchema, +} from "@repo/db/usedSchemas"; +import { ClaimStatus } from "./claims-recent-table"; + +type Claim = z.infer; +type Staff = z.infer; + +type ClaimWithServiceLines = Claim & { + serviceLines: { + id: number; + claimId: number; + procedureCode: string; + procedureDate: Date; + oralCavityArea: string | null; + toothNumber: string | null; + toothSurface: string | null; + billedAmount: number; + }[]; + staff: Staff | null; +}; + +type ClaimEditModalProps = { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onClose: () => void; + claim: ClaimWithServiceLines | null; + onSave: (updatedClaim: ClaimWithServiceLines) => void; +}; + +export default function ClaimEditModal({ + isOpen, + onOpenChange, + onClose, + claim, + onSave, +}: ClaimEditModalProps) { + const [status, setStatus] = useState( + claim?.status ?? ("PENDING" as ClaimStatus) + ); + + if (!claim) return null; + + const handleSave = () => { + const updatedClaim: ClaimWithServiceLines = { + ...claim, + status, + }; + + onSave(updatedClaim); + onOpenChange(false); + }; + + return ( + + + + Edit Claim Status + Update the status of the claim. + + +
+ {/* Patient Details */} +
+
+ {claim.patientName.charAt(0)} +
+
+

{claim.patientName}

+

+ Claim ID: {claim.id?.toString().padStart(4, "0")} +

+
+
+ + {/* Basic Info */} +
+
+

Basic Information

+
+

+ Date of Birth:{" "} + {new Date(claim.dateOfBirth).toLocaleDateString()} +

+

+ Service Date:{" "} + {new Date(claim.serviceDate).toLocaleDateString()} +

+
+ Status: + +
+
+
+ +
+

Insurance Details

+
+

+ Insurance Provider:{" "} + {claim.insuranceProvider || "N/A"} +

+

+ Member ID:{" "} + {claim.memberId} +

+

+ Remarks:{" "} + {claim.remarks || "N/A"} +

+
+
+
+ + {/* Timestamps */} +
+

Timestamps

+

+ Created At:{" "} + {formatDateToHumanReadable(claim.createdAt)} +

+

+ Updated At:{" "} + {formatDateToHumanReadable(claim.updatedAt)} +

+
+ + {/* Staff Info */} + {claim.staff && ( +
+

Assigned Staff

+

+ Name: {claim.staff.name} +

+

+ Role: {claim.staff.role} +

+ {claim.staff.email && ( +

+ Email:{" "} + {claim.staff.email} +

+ )} + {claim.staff.phone && ( +

+ Phone:{" "} + {claim.staff.phone} +

+ )} +
+ )} + + {/* Service Lines */} +
+

Service Lines

+
+ {claim.serviceLines.length > 0 ? ( + <> + {claim.serviceLines.map((line) => ( +
+

+ Procedure Code:{" "} + {line.procedureCode} +

+

+ Procedure Date:{" "} + {new Date(line.procedureDate).toLocaleDateString()} +

+ {line.oralCavityArea && ( +

+ + Oral Cavity Area: + {" "} + {line.oralCavityArea} +

+ )} + {line.toothNumber && ( +

+ Tooth Number:{" "} + {line.toothNumber} +

+ )} + {line.toothSurface && ( +

+ Tooth Surface:{" "} + {line.toothSurface} +

+ )} +

+ Billed Amount: $ + {line.billedAmount.toFixed(2)} +

+
+ ))} +
+ Total Billed Amount: $ + {claim.serviceLines + .reduce((total, line) => total + line.billedAmount, 0) + .toFixed(2)} +
+ + ) : ( +

No service lines available.

+ )} +
+
+ + {/* Actions */} +
+ + +
+
+
+
+ ); +} diff --git a/apps/Frontend/src/components/claims/claim-view-modal.tsx b/apps/Frontend/src/components/claims/claim-view-modal.tsx index e8a42c6..7c9d2a2 100644 --- a/apps/Frontend/src/components/claims/claim-view-modal.tsx +++ b/apps/Frontend/src/components/claims/claim-view-modal.tsx @@ -6,12 +6,17 @@ import { DialogDescription, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; +import { + ClaimUncheckedCreateInputObjectSchema, + StaffUncheckedCreateInputObjectSchema, +} from "@repo/db/usedSchemas"; import React from "react"; import { z } from "zod"; +import { formatDateToHumanReadable } from "@/utils/dateUtils"; //creating types out of schema auto generated. type Claim = z.infer; +type Staff = z.infer; type ClaimWithServiceLines = Claim & { serviceLines: { @@ -24,6 +29,7 @@ type ClaimWithServiceLines = Claim & { toothSurface: string | null; billedAmount: number; }[]; + staff: Staff | null; }; type ClaimViewModalProps = { @@ -43,8 +49,7 @@ export default function ClaimViewModal({ }: ClaimViewModalProps) { return ( - - + Claim Details @@ -70,10 +75,6 @@ export default function ClaimViewModal({

Basic Information

-

- Member ID:{" "} - {claim.memberId} -

Date of Birth:{" "} {new Date(claim.dateOfBirth).toLocaleDateString()} @@ -105,12 +106,16 @@ export default function ClaimViewModal({

-

Insurance

+

Insurance Details

- Provider:{" "} + Insurance Provider:{" "} {claim.insuranceProvider || "N/A"}

+

+ Member ID:{" "} + {claim.memberId} +

Remarks:{" "} {claim.remarks || "N/A"} @@ -119,49 +124,98 @@ export default function ClaimViewModal({

+ {/* Metadata */} +
+

Timestamps

+

+ Created At:{" "} + {formatDateToHumanReadable(claim.createdAt)} +

+

+ Updated At:{" "} + {formatDateToHumanReadable(claim.updatedAt)} +

+
+ + {claim.staff && ( +
+

Assigned Staff

+

+ Name:{" "} + {claim.staff.name} +

+

+ Role:{" "} + {claim.staff.role} +

+ {claim.staff.email && ( +

+ Email:{" "} + {claim.staff.email} +

+ )} + {claim.staff.phone && ( +

+ Phone:{" "} + {claim.staff.phone} +

+ )} +
+ )} +

Service Lines

{claim.serviceLines.length > 0 ? ( - claim.serviceLines.map((line, index) => ( -
-

- Procedure Code:{" "} - {line.procedureCode} -

-

- Procedure Date:{" "} - {new Date(line.procedureDate).toLocaleDateString()} -

- {line.oralCavityArea && ( + <> + {claim.serviceLines.map((line, index) => ( +

- - Oral Cavity Area: - {" "} - {line.oralCavityArea} + Procedure Code:{" "} + {line.procedureCode}

- )} - {line.toothNumber && (

- Tooth Number:{" "} - {line.toothNumber} + Procedure Date:{" "} + {new Date(line.procedureDate).toLocaleDateString()}

- )} - {line.toothSurface && ( + {line.oralCavityArea && ( +

+ + Oral Cavity Area: + {" "} + {line.oralCavityArea} +

+ )} + {line.toothNumber && ( +

+ Tooth Number:{" "} + {line.toothNumber} +

+ )} + {line.toothSurface && ( +

+ + Tooth Surface: + {" "} + {line.toothSurface} +

+ )}

- Tooth Surface:{" "} - {line.toothSurface} + Billed Amount:{" "} + ${line.billedAmount.toFixed(2)}

- )} -

- Billed Amount: $ - {line.billedAmount.toFixed(2)} -

+
+ ))} +
+ Total Billed Amount: $ + {claim.serviceLines + .reduce((total, line) => total + line.billedAmount, 0) + .toFixed(2)}
- )) + ) : (

No service lines available.

)} diff --git a/apps/Frontend/src/components/claims/claims-recent-table.tsx b/apps/Frontend/src/components/claims/claims-recent-table.tsx index 70b5ffa..e428ee7 100644 --- a/apps/Frontend/src/components/claims/claims-recent-table.tsx +++ b/apps/Frontend/src/components/claims/claims-recent-table.tsx @@ -32,6 +32,7 @@ import { PatientUncheckedCreateInputObjectSchema, ClaimUncheckedCreateInputObjectSchema, ClaimStatusSchema, + StaffUncheckedCreateInputObjectSchema, } from "@repo/db/usedSchemas"; import { z } from "zod"; import { useAuth } from "@/hooks/use-auth"; @@ -42,10 +43,12 @@ import { cn } from "@/lib/utils"; import { formatDateToHumanReadable } from "@/utils/dateUtils"; import { Card, CardHeader, CardTitle } from "@/components/ui/card"; import ClaimViewModal from "./claim-view-modal"; +import ClaimEditModal from "./claim-edit-modal"; //creating types out of schema auto generated. type Claim = z.infer; export type ClaimStatus = z.infer; +type Staff = z.infer; type ClaimWithServiceLines = Claim & { serviceLines: { @@ -58,6 +61,7 @@ type ClaimWithServiceLines = Claim & { toothSurface: string | null; billedAmount: number; }[]; + staff: Staff | null; }; const PatientSchema = ( @@ -140,6 +144,42 @@ export default function ClaimsRecentTable({ placeholderData: { claims: [], totalCount: 0 }, }); + const updateClaimMutation = useMutation({ + mutationFn: async (claim: ClaimWithServiceLines) => { + const response = await apiRequest("PUT", `/api/claims/${claim.id}`, { + status: claim.status, + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "Failed to update claim"); + } + return response.json(); + }, + onSuccess: () => { + setIsEditClaimOpen(false); + toast({ + title: "Success", + description: "Claim updated successfully!", + variant: "default", + }); + queryClient.invalidateQueries({ + queryKey: [ + "claims-recent", + { + page: currentPage, + }, + ], + }); + }, + onError: (error) => { + toast({ + title: "Error", + description: `Update failed: ${error.message}`, + variant: "destructive", + }); + }, + }); + const deleteClaimMutation = useMutation({ mutationFn: async (id: number) => { await apiRequest("DELETE", `/api/claims/${id}`); @@ -475,6 +515,18 @@ export default function ClaimsRecentTable({ /> )} + {isEditClaimOpen && currentClaim && ( + setIsEditClaimOpen(false)} + onOpenChange={(open) => setIsEditClaimOpen(open)} + claim={currentClaim} + onSave={(updatedClaim) => { + updateClaimMutation.mutate(updatedClaim); + }} + /> + )} + {/* Pagination */} {totalPages > 1 && (
diff --git a/apps/Frontend/src/utils/dateUtils.ts b/apps/Frontend/src/utils/dateUtils.ts index a0cc0f5..f98ee67 100644 --- a/apps/Frontend/src/utils/dateUtils.ts +++ b/apps/Frontend/src/utils/dateUtils.ts @@ -80,7 +80,10 @@ export function normalizeToISOString(date: Date | string): string { * @param dateInput The date as a string (e.g., ISO, YYYY-MM-DD) or a Date object. * @returns A formatted date string. */ -export const formatDateToHumanReadable = (dateInput: string | Date): string => { +export const formatDateToHumanReadable = ( + dateInput?: string | Date +): string => { + if (!dateInput) return "N/A"; // Create a Date object from the input. // The Date constructor is quite flexible with various string formats. const date = new Date(dateInput);