types updated

This commit is contained in:
2025-08-10 18:21:38 +05:30
parent 31ed4cd1da
commit 4b1ee273e4
44 changed files with 2049 additions and 1263 deletions

39
ReadmeForDBmigrate.txt Normal file
View File

@@ -0,0 +1,39 @@
while updating whole schema for payment,
firstly just update this line in schema: and follow all steps. then update other schema from github.
totalBilled Decimal @db.Decimal(10, 2)
1. firstly create backup:
pg_dump -U your_db_user -h localhost -p 5432 your_db_name > backup_before_totalBilled.sql
$ pg_dump -U postgres -h localhost -p 5432 dentalapp > backup_before_totalBilled.sql
2. - now update the schema:
totalBilled Decimal @db.Decimal(10, 2)
3. create migration not apply yet
npx prisma migrate dev --create-only --name rename-billedamount-to-totalbilled
4. edit migration.sql file:
replace whatever prisma put there:
ALTER TABLE "public"."ServiceLine"
RENAME COLUMN "billedAmount" TO "totalBilled";
ALTER TABLE "public"."ServiceLine"
ALTER COLUMN "totalBilled" TYPE DECIMAL(10,2) USING "totalBilled"::DECIMAL(10,2);
5. npx prisma migrate dev
6. if anythign goes wrong ,do restore backup.
psql -U your_db_user -h localhost -p 5432 your_db_name < backup_before_totalBilled.sql

View File

@@ -2,73 +2,8 @@ import { Router } from "express";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { storage } from "../storage"; import { storage } from "../storage";
import { z } from "zod"; import { z } from "zod";
import {
ClaimUncheckedCreateInputObjectSchema,
PaymentUncheckedCreateInputObjectSchema,
PaymentTransactionCreateInputObjectSchema,
ServiceLinePaymentCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import { Prisma } from "@repo/db/generated/prisma";
import { ZodError } from "zod"; import { ZodError } from "zod";
import { insertPaymentSchema, updatePaymentSchema } from "@repo/db/types";
// Base Payment type
type Payment = z.infer<typeof PaymentUncheckedCreateInputObjectSchema>;
type PaymentTransaction = z.infer<
typeof PaymentTransactionCreateInputObjectSchema
>;
type ServiceLinePayment = z.infer<
typeof ServiceLinePaymentCreateInputObjectSchema
>;
const insertPaymentSchema = (
PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
updatedAt: true,
});
type InsertPayment = z.infer<typeof insertPaymentSchema>;
const updatePaymentSchema = (
PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
type UpdatePayment = z.infer<typeof updatePaymentSchema>;
type PaymentWithExtras = Prisma.PaymentGetPayload<{
include: {
transactions: true;
servicePayments: true;
claim: true;
};
}>;
// Claim schema
const ClaimSchema = (
ClaimUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
updatedAt: true,
});
type InsertClaim = z.infer<typeof ClaimSchema>;
const updateClaimSchema = (
ClaimUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
updatedAt: true,
})
.partial();
type UpdateClaim = z.infer<typeof updateClaimSchema>;
const paymentFilterSchema = z.object({ const paymentFilterSchema = z.object({
from: z.string().datetime(), from: z.string().datetime(),

View File

@@ -1,165 +1,29 @@
import { prisma as db } from "@repo/db/client"; import { prisma as db } from "@repo/db/client";
import { PdfCategory } from "@repo/db/generated/prisma";
import { import {
AppointmentUncheckedCreateInputObjectSchema, Appointment,
PatientUncheckedCreateInputObjectSchema, Claim,
UserUncheckedCreateInputObjectSchema, ClaimWithServiceLines,
StaffUncheckedCreateInputObjectSchema, InsertAppointment,
ClaimUncheckedCreateInputObjectSchema, InsertClaim,
InsuranceCredentialUncheckedCreateInputObjectSchema, InsertInsuranceCredential,
PdfFileUncheckedCreateInputObjectSchema, InsertPatient,
PdfGroupUncheckedCreateInputObjectSchema,
PdfCategorySchema,
} from "@repo/db/usedSchemas";
import { z } from "zod";
import {
InsertPayment, InsertPayment,
InsertUser,
InsuranceCredential,
Patient,
Payment, Payment,
PaymentWithExtras, PaymentWithExtras,
PdfFile,
PdfGroup,
Staff,
UpdateAppointment,
UpdateClaim,
UpdatePatient,
UpdatePayment, UpdatePayment,
User,
} from "@repo/db/types"; } from "@repo/db/types";
//creating types out of schema auto generated.
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
const insertAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
//patient types
type Patient = z.infer<typeof PatientUncheckedCreateInputObjectSchema>;
const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>;
//user types
type User = z.infer<typeof UserUncheckedCreateInputObjectSchema>;
const insertUserSchema = (
UserUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).pick({
username: true,
password: true,
});
const loginSchema = (insertUserSchema as unknown as z.ZodObject<any>).extend({
rememberMe: z.boolean().optional(),
});
const registerSchema = (insertUserSchema as unknown as z.ZodObject<any>)
.extend({
confirmPassword: z.string().min(6, {
message: "Password must be at least 6 characters long",
}),
agreeTerms: z.literal(true, {
errorMap: () => ({
message: "You must agree to the terms and conditions",
}),
}),
})
.refine((data: any) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
type InsertUser = z.infer<typeof insertUserSchema>;
type LoginFormValues = z.infer<typeof loginSchema>;
type RegisterFormValues = z.infer<typeof registerSchema>;
// staff types:
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
// Claim typse:
const insertClaimSchema = (
ClaimUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
updatedAt: true,
});
type InsertClaim = z.infer<typeof insertClaimSchema>;
const updateClaimSchema = (
ClaimUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
updatedAt: true,
})
.partial();
type UpdateClaim = z.infer<typeof updateClaimSchema>;
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
// InsuraneCreds types:
type InsuranceCredential = z.infer<
typeof InsuranceCredentialUncheckedCreateInputObjectSchema
>;
const insertInsuranceCredentialSchema = (
InsuranceCredentialUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({ id: true });
type InsertInsuranceCredential = z.infer<
typeof insertInsuranceCredentialSchema
>;
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;
};
// Pdf types:
type PdfGroup = z.infer<typeof PdfGroupUncheckedCreateInputObjectSchema>;
type PdfFile = z.infer<typeof PdfFileUncheckedCreateInputObjectSchema>;
type PdfCategory = z.infer<typeof PdfCategorySchema>;
export interface ClaimPdfMetadata {
id: number;
filename: string;
uploadedAt: Date;
}
export interface IStorage { export interface IStorage {
// User methods // User methods
getUser(id: number): Promise<User | undefined>; getUser(id: number): Promise<User | undefined>;

View File

@@ -1,33 +1,22 @@
import { useState } from "react"; import {
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { AppointmentForm } from "./appointment-form"; import { AppointmentForm } from "./appointment-form";
import { AppointmentUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; import {
Appointment,
import {z} from "zod"; InsertAppointment,
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>; Patient,
UpdateAppointment,
const insertAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({ } from "@repo/db/types";
id: true,
createdAt: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
id: true,
createdAt: true,
}).partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
interface AddAppointmentModalProps { interface AddAppointmentModalProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onSubmit: (data: InsertAppointment | UpdateAppointment) => void; onSubmit: (data: InsertAppointment | UpdateAppointment) => void;
onDelete?: (id: number) => void; onDelete?: (id: number) => void;
isLoading: boolean; isLoading: boolean;
appointment?: Appointment; appointment?: Appointment;
patients: Patient[]; patients: Patient[];
@@ -42,7 +31,6 @@ export function AddAppointmentModal({
appointment, appointment,
patients, patients,
}: AddAppointmentModalProps) { }: AddAppointmentModalProps) {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md"> <DialogContent className="max-w-md">
@@ -61,10 +49,10 @@ export function AddAppointmentModal({
}} }}
isLoading={isLoading} isLoading={isLoading}
onDelete={onDelete} onDelete={onDelete}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
/> />
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
} }

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns"; import { format } from "date-fns";
@@ -33,41 +33,13 @@ import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { useDebounce } from "use-debounce"; import { useDebounce } from "use-debounce";
import { import {
AppointmentUncheckedCreateInputObjectSchema, Appointment,
PatientUncheckedCreateInputObjectSchema, InsertAppointment,
StaffUncheckedCreateInputObjectSchema, insertAppointmentSchema,
} from "@repo/db/usedSchemas"; Patient,
Staff,
import { z } from "zod"; UpdateAppointment,
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>; } from "@repo/db/types";
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
const insertAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
userId: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
interface AppointmentFormProps { interface AppointmentFormProps {
appointment?: Appointment; appointment?: Appointment;
@@ -211,7 +183,7 @@ export function AppointmentForm({
const term = debouncedSearchTerm.toLowerCase(); const term = debouncedSearchTerm.toLowerCase();
setFilteredPatients( setFilteredPatients(
patients.filter((p) => patients.filter((p) =>
`${p.firstName} ${p.lastName} ${p.phone} ${p.dob}` `${p.firstName} ${p.lastName} ${p.phone} ${p.dateOfBirth}`
.toLowerCase() .toLowerCase()
.includes(term) .includes(term)
) )
@@ -351,7 +323,7 @@ export function AppointmentForm({
filteredPatients.map((patient) => ( filteredPatients.map((patient) => (
<SelectItem <SelectItem
key={patient.id} key={patient.id}
value={patient.id.toString()} value={patient.id?.toString() ?? ""}
> >
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-medium"> <span className="font-medium">
@@ -435,7 +407,7 @@ export function AppointmentForm({
selected={field.value} selected={field.value}
onSelect={(date) => { onSelect={(date) => {
if (date) { if (date) {
field.onChange(date); field.onChange(date);
} }
}} }}
disabled={(date: Date) => disabled={(date: Date) =>

View File

@@ -25,20 +25,7 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { import { Appointment, Patient } from "@repo/db/types";
AppointmentUncheckedCreateInputObjectSchema,
PatientUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import { z } from "zod";
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
interface AppointmentTableProps { interface AppointmentTableProps {
appointments: Appointment[]; appointments: Appointment[];

View File

@@ -15,29 +15,7 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { formatDateToHumanReadable } from "@/utils/dateUtils"; import { formatDateToHumanReadable } from "@/utils/dateUtils";
import React, { useState } from "react"; import React, { useState } from "react";
import { z } from "zod"; import { ClaimStatus, ClaimWithServiceLines } from "@repo/db/types";
import {
ClaimUncheckedCreateInputObjectSchema,
StaffUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import { ClaimStatus } from "./claims-recent-table";
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
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 = { type ClaimEditModalProps = {
isOpen: boolean; isOpen: boolean;
@@ -223,14 +201,17 @@ export default function ClaimEditModal({
)} )}
<p> <p>
<span className="text-gray-500">Billed Amount:</span> $ <span className="text-gray-500">Billed Amount:</span> $
{line.billedAmount.toFixed(2)} {line.totalBilled.toFixed(2)}
</p> </p>
</div> </div>
))} ))}
<div className="text-right font-semibold text-gray-900 pt-2 border-t mt-4"> <div className="text-right font-semibold text-gray-900 pt-2 border-t mt-4">
Total Billed Amount: $ Total Billed Amount: $
{claim.serviceLines {claim.serviceLines
.reduce((total, line) => total + line.billedAmount, 0) .reduce(
(total, line) => total + line.totalBilled?.toNumber(),
0
)
.toFixed(2)} .toFixed(2)}
</div> </div>
</> </>

View File

@@ -18,14 +18,6 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import {
PatientUncheckedCreateInputObjectSchema,
AppointmentUncheckedCreateInputObjectSchema,
ClaimUncheckedCreateInputObjectSchema,
ClaimStatusSchema,
StaffUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import { z } from "zod";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@/lib/queryClient"; import { apiRequest } from "@/lib/queryClient";
import { MultipleFileUploadZone } from "../file-upload/multiple-file-upload-zone"; import { MultipleFileUploadZone } from "../file-upload/multiple-file-upload-zone";
@@ -37,57 +29,16 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import procedureCodes from "../../assets/data/procedureCodes.json"; import procedureCodes from "../../assets/data/procedureCodes.json";
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
import {
const PatientSchema = ( Claim,
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> InputServiceLine,
).omit({ InsertAppointment,
appointments: true, Patient,
}); Staff,
type Patient = z.infer<typeof PatientSchema>; UpdateAppointment,
UpdatePatient,
const updatePatientSchema = ( } from "@repo/db/types";
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> import { Decimal } from "decimal.js";
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>;
//creating types out of schema auto generated.
const insertAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
interface ServiceLine {
procedureCode: string;
procedureDate: string; // YYYY-MM-DD
oralCavityArea?: string;
toothNumber?: string;
toothSurface?: string;
billedAmount: number;
}
interface ClaimFormData { interface ClaimFormData {
patientId: number; patientId: number;
@@ -102,7 +53,7 @@ interface ClaimFormData {
insuranceProvider: string; insuranceProvider: string;
insuranceSiteKey?: string; insuranceSiteKey?: string;
status: string; // default "pending" status: string; // default "pending"
serviceLines: ServiceLine[]; serviceLines: InputServiceLine[];
claimId?: number; claimId?: number;
} }
@@ -117,9 +68,6 @@ interface ClaimFormProps {
onClose: () => void; onClose: () => void;
} }
export type ClaimStatus = z.infer<typeof ClaimStatusSchema>;
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
export function ClaimForm({ export function ClaimForm({
patientId, patientId,
onHandleAppointmentSubmit, onHandleAppointmentSubmit,
@@ -207,16 +155,21 @@ export function ClaimForm({
}, [serviceDate]); }, [serviceDate]);
// Determine patient date of birth format - required as date extracted from pdfs has different format. // Determine patient date of birth format - required as date extracted from pdfs has different format.
const formatDOB = (dob: string | undefined) => { const formatDOB = (dob: string | Date | undefined) => {
if (!dob) return ""; if (!dob) return "";
if (/^\d{2}\/\d{2}\/\d{4}$/.test(dob)) return dob; // already MM/DD/YYYY
if (/^\d{4}-\d{2}-\d{2}/.test(dob)) { const normalized = formatLocalDate(parseLocalDate(dob));
const datePart = dob?.split("T")[0]; // safe optional chaining
if (!datePart) return ""; // If it's already MM/DD/YYYY, leave it alone
const [year, month, day] = datePart.split("-"); if (/^\d{2}\/\d{2}\/\d{4}$/.test(normalized)) return normalized;
// If it's yyyy-MM-dd, swap order to MM/DD/YYYY
if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
const [year, month, day] = normalized.split("-");
return `${month}/${day}/${year}`; return `${month}/${day}/${year}`;
} }
return dob;
return normalized;
}; };
// MAIN FORM INITIAL STATE // MAIN FORM INITIAL STATE
@@ -226,7 +179,7 @@ export function ClaimForm({
userId: Number(user?.id), userId: Number(user?.id),
staffId: Number(staff?.id), staffId: Number(staff?.id),
patientName: `${patient?.firstName} ${patient?.lastName}`.trim(), patientName: `${patient?.firstName} ${patient?.lastName}`.trim(),
memberId: patient?.insuranceId, memberId: patient?.insuranceId ?? "",
dateOfBirth: formatDOB(patient?.dateOfBirth), dateOfBirth: formatDOB(patient?.dateOfBirth),
remarks: "", remarks: "",
serviceDate: serviceDate, serviceDate: serviceDate,
@@ -239,7 +192,9 @@ export function ClaimForm({
oralCavityArea: "", oralCavityArea: "",
toothNumber: "", toothNumber: "",
toothSurface: "", toothSurface: "",
billedAmount: 0, totalBilled: new Decimal(0),
totalAdjusted: new Decimal(0),
totalPaid: new Decimal(0),
})), })),
uploadedFiles: [], uploadedFiles: [],
}); });
@@ -251,10 +206,10 @@ export function ClaimForm({
`${patient.firstName || ""} ${patient.lastName || ""}`.trim(); `${patient.firstName || ""} ${patient.lastName || ""}`.trim();
setForm((prev) => ({ setForm((prev) => ({
...prev, ...prev,
patientId: Number(patient.id),
patientName: fullName, patientName: fullName,
dateOfBirth: formatDOB(patient.dateOfBirth), dateOfBirth: formatDOB(patient.dateOfBirth),
memberId: patient.insuranceId || "", memberId: patient.insuranceId || "",
patientId: patient.id,
})); }));
} }
}, [patient]); }, [patient]);
@@ -266,16 +221,16 @@ export function ClaimForm({
const updateServiceLine = ( const updateServiceLine = (
index: number, index: number,
field: keyof ServiceLine, field: keyof InputServiceLine,
value: any value: any
) => { ) => {
const updatedLines = [...form.serviceLines]; const updatedLines = [...form.serviceLines];
if (updatedLines[index]) { if (updatedLines[index]) {
if (field === "billedAmount") { if (field === "totalBilled") {
const num = typeof value === "string" ? parseFloat(value) : value; const num = typeof value === "string" ? parseFloat(value) : value;
const rounded = Math.round((isNaN(num) ? 0 : num) * 100) / 100; const rounded = Math.round((isNaN(num) ? 0 : num) * 100) / 100;
updatedLines[index][field] = rounded; updatedLines[index][field] = new Decimal(rounded);
} else { } else {
updatedLines[index][field] = value; updatedLines[index][field] = value;
} }
@@ -672,16 +627,20 @@ export function ClaimForm({
type="number" type="number"
step="0.01" step="0.01"
placeholder="$0.00" placeholder="$0.00"
value={line.billedAmount === 0 ? "" : line.billedAmount} value={
line.totalBilled?.toNumber() === 0
? ""
: line.totalBilled?.toNumber()
}
onChange={(e) => { onChange={(e) => {
updateServiceLine(i, "billedAmount", e.target.value); updateServiceLine(i, "totalBilled", e.target.value);
}} }}
onBlur={(e) => { onBlur={(e) => {
const val = parseFloat(e.target.value); const val = parseFloat(e.target.value);
const rounded = Math.round(val * 100) / 100; const rounded = Math.round(val * 100) / 100;
updateServiceLine( updateServiceLine(
i, i,
"billedAmount", "totalBilled",
isNaN(rounded) ? 0 : rounded isNaN(rounded) ? 0 : rounded
); );
}} }}
@@ -702,7 +661,9 @@ export function ClaimForm({
oralCavityArea: "", oralCavityArea: "",
toothNumber: "", toothNumber: "",
toothSurface: "", toothSurface: "",
billedAmount: 0, totalBilled: new Decimal(0),
totalAdjusted: new Decimal(0),
totalPaid: new Decimal(0),
}, },
], ],
})) }))

View File

@@ -6,31 +6,9 @@ import {
DialogDescription, DialogDescription,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
ClaimUncheckedCreateInputObjectSchema,
StaffUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import React from "react"; import React from "react";
import { z } from "zod";
import { formatDateToHumanReadable } from "@/utils/dateUtils"; import { formatDateToHumanReadable } from "@/utils/dateUtils";
import { ClaimWithServiceLines } from "@repo/db/types";
//creating types out of schema auto generated.
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
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 ClaimViewModalProps = { type ClaimViewModalProps = {
isOpen: boolean; isOpen: boolean;
@@ -205,14 +183,17 @@ export default function ClaimViewModal({
)} )}
<p> <p>
<span className="text-gray-500">Billed Amount:</span>{" "} <span className="text-gray-500">Billed Amount:</span>{" "}
${line.billedAmount.toFixed(2)} ${line.totalBilled.toFixed(2)}
</p> </p>
</div> </div>
))} ))}
<div className="text-right font-semibold text-gray-900 pt-2 border-t mt-4"> <div className="text-right font-semibold text-gray-900 pt-2 border-t mt-4">
Total Billed Amount: $ Total Billed Amount: $
{claim.serviceLines {claim.serviceLines
.reduce((total, line) => total + line.billedAmount, 0) .reduce(
(total, line) => total + line.totalBilled?.toNumber(),
0
)
.toFixed(2)} .toFixed(2)}
</div> </div>
</> </>

View File

@@ -1,8 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import ClaimsRecentTable from "./claims-recent-table"; import ClaimsRecentTable from "./claims-recent-table";
import { PatientTable } from "../patients/patient-table"; import { PatientTable } from "../patients/patient-table";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import { z } from "zod";
import { import {
Card, Card,
CardContent, CardContent,
@@ -10,19 +8,15 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "../ui/card"; } from "../ui/card";
import { Patient } from "@repo/db/types";
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
interface ClaimsOfPatientModalProps { interface ClaimsOfPatientModalProps {
onNewClaim?: (patientId: number) => void; onNewClaim?: (patientId: number) => void;
} }
export default function ClaimsOfPatientModal({ onNewClaim }: ClaimsOfPatientModalProps) { export default function ClaimsOfPatientModal({
onNewClaim,
}: ClaimsOfPatientModalProps) {
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null); const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [claimsPage, setClaimsPage] = useState(1); const [claimsPage, setClaimsPage] = useState(1);

View File

@@ -28,45 +28,13 @@ import {
PaginationPrevious, PaginationPrevious,
} from "@/components/ui/pagination"; } from "@/components/ui/pagination";
import { DeleteConfirmationDialog } from "../ui/deleteDialog"; import { DeleteConfirmationDialog } from "../ui/deleteDialog";
import {
PatientUncheckedCreateInputObjectSchema,
ClaimUncheckedCreateInputObjectSchema,
ClaimStatusSchema,
StaffUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import { z } from "zod";
import LoadingScreen from "@/components/ui/LoadingScreen"; import LoadingScreen from "@/components/ui/LoadingScreen";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { formatDateToHumanReadable } from "@/utils/dateUtils"; import { formatDateToHumanReadable } from "@/utils/dateUtils";
import ClaimViewModal from "./claim-view-modal"; import ClaimViewModal from "./claim-view-modal";
import ClaimEditModal from "./claim-edit-modal"; import ClaimEditModal from "./claim-edit-modal";
import { Claim, ClaimStatus, ClaimWithServiceLines } from "@repo/db/types";
//creating types out of schema auto generated.
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
export type ClaimStatus = z.infer<typeof ClaimStatusSchema>;
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
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;
};
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
interface ClaimApiResponse { interface ClaimApiResponse {
claims: ClaimWithServiceLines[]; claims: ClaimWithServiceLines[];
@@ -314,7 +282,7 @@ export default function ClaimsRecentTable({
const getTotalBilled = (claim: ClaimWithServiceLines) => { const getTotalBilled = (claim: ClaimWithServiceLines) => {
return claim.serviceLines.reduce( return claim.serviceLines.reduce(
(sum, line) => sum + (line.billedAmount || 0), (sum, line) => sum + (line.totalBilled?.toNumber() || 0),
0 0
); );
}; };

View File

@@ -1,8 +1,8 @@
import React, { useState, useRef, useCallback } from 'react'; import React, { useState, useRef, useCallback } from "react";
import { Upload, File, X, FilePlus } from 'lucide-react'; import { Upload, File, X, FilePlus } from "lucide-react";
import { Button } from '@/components/ui/button'; import { Button } from "@/components/ui/button";
import { useToast } from '@/hooks/use-toast'; import { useToast } from "@/hooks/use-toast";
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
interface FileUploadZoneProps { interface FileUploadZoneProps {
onFileUpload: (file: File) => void; onFileUpload: (file: File) => void;
@@ -10,10 +10,10 @@ interface FileUploadZoneProps {
acceptedFileTypes?: string; acceptedFileTypes?: string;
} }
export function FileUploadZone({ export function FileUploadZone({
onFileUpload, onFileUpload,
isUploading, isUploading,
acceptedFileTypes = "application/pdf" acceptedFileTypes = "application/pdf",
}: FileUploadZoneProps) { }: FileUploadZoneProps) {
const { toast } = useToast(); const { toast } = useToast();
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
@@ -32,13 +32,16 @@ export function FileUploadZone({
setIsDragging(false); setIsDragging(false);
}, []); }, []);
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => { const handleDragOver = useCallback(
e.preventDefault(); (e: React.DragEvent<HTMLDivElement>) => {
e.stopPropagation(); e.preventDefault();
if (!isDragging) { e.stopPropagation();
setIsDragging(true); if (!isDragging) {
} setIsDragging(true);
}, [isDragging]); }
},
[isDragging]
);
const validateFile = (file: File) => { const validateFile = (file: File) => {
// Check file type // Check file type
@@ -46,49 +49,55 @@ export function FileUploadZone({
toast({ toast({
title: "Invalid file type", title: "Invalid file type",
description: "Please upload a PDF file.", description: "Please upload a PDF file.",
variant: "destructive" variant: "destructive",
}); });
return false; return false;
} }
// Check file size (limit to 5MB) // Check file size (limit to 5MB)
if (file.size > 5 * 1024 * 1024) { if (file.size > 5 * 1024 * 1024) {
toast({ toast({
title: "File too large", title: "File too large",
description: "File size should be less than 5MB.", description: "File size should be less than 5MB.",
variant: "destructive" variant: "destructive",
}); });
return false; return false;
} }
return true; return true;
}; };
const handleDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => { const handleDrop = useCallback(
e.preventDefault(); (e: React.DragEvent<HTMLDivElement>) => {
e.stopPropagation(); e.preventDefault();
setIsDragging(false); e.stopPropagation();
setIsDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
const file = e.dataTransfer.files[0];
if (validateFile(file)) {
setUploadedFile(file);
onFileUpload(file);
}
}
}, [onFileUpload, acceptedFileTypes, toast]);
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { if (e.dataTransfer.files && e.dataTransfer.files[0]) {
if (e.target.files && e.target.files[0]) { const file = e.dataTransfer.files[0];
const file = e.target.files[0];
if (validateFile(file)) {
if (validateFile(file)) { setUploadedFile(file);
setUploadedFile(file); onFileUpload(file);
onFileUpload(file); }
} }
} },
}, [onFileUpload, acceptedFileTypes, toast]); [onFileUpload, acceptedFileTypes, toast]
);
const handleFileSelect = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
if (validateFile(file)) {
setUploadedFile(file);
onFileUpload(file);
}
}
},
[onFileUpload, acceptedFileTypes, toast]
);
const handleBrowseClick = () => { const handleBrowseClick = () => {
if (fileInputRef.current) { if (fileInputRef.current) {
@@ -109,11 +118,13 @@ export function FileUploadZone({
onChange={handleFileSelect} onChange={handleFileSelect}
accept={acceptedFileTypes} accept={acceptedFileTypes}
/> />
<div <div
className={cn( className={cn(
"border-2 border-dashed rounded-lg p-8 flex flex-col items-center justify-center text-center transition-colors", "border-2 border-dashed rounded-lg p-8 flex flex-col items-center justify-center text-center transition-colors",
isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25", isDragging
? "border-primary bg-primary/5"
: "border-muted-foreground/25",
uploadedFile ? "bg-success/5" : "hover:bg-muted/40", uploadedFile ? "bg-success/5" : "hover:bg-muted/40",
isUploading && "opacity-50 cursor-not-allowed" isUploading && "opacity-50 cursor-not-allowed"
)} )}
@@ -135,7 +146,7 @@ export function FileUploadZone({
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<div className="relative"> <div className="relative">
<File className="h-12 w-12 text-primary" /> <File className="h-12 w-12 text-primary" />
<button <button
className="absolute -top-2 -right-2 bg-background rounded-full p-1 shadow-sm border" className="absolute -top-2 -right-2 bg-background rounded-full p-1 shadow-sm border"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -166,9 +177,9 @@ export function FileUploadZone({
Or click to browse files Or click to browse files
</p> </p>
</div> </div>
<Button <Button
type="button" type="button"
variant="secondary" variant="secondary"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleBrowseClick(); handleBrowseClick();
@@ -184,4 +195,4 @@ export function FileUploadZone({
</div> </div>
</div> </div>
); );
} }

View File

@@ -20,12 +20,12 @@ interface CredentialsModalProps {
isLoading?: boolean; isLoading?: boolean;
} }
export function CredentialsModal({ export function CredentialsModal({
isOpen, isOpen,
onClose, onClose,
onSubmit, onSubmit,
providerName, providerName,
isLoading = false isLoading = false,
}: CredentialsModalProps) { }: CredentialsModalProps) {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@@ -51,10 +51,11 @@ export function CredentialsModal({
<DialogHeader> <DialogHeader>
<DialogTitle>Insurance Portal Login</DialogTitle> <DialogTitle>Insurance Portal Login</DialogTitle>
<DialogDescription> <DialogDescription>
Enter your credentials for {providerName} insurance portal to check patient eligibility automatically. Enter your credentials for {providerName} insurance portal to check
patient eligibility automatically.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="username">Username</Label> <Label htmlFor="username">Username</Label>
@@ -68,7 +69,7 @@ export function CredentialsModal({
disabled={isLoading} disabled={isLoading}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="password">Password</Label> <Label htmlFor="password">Password</Label>
<div className="relative"> <div className="relative">
@@ -97,7 +98,7 @@ export function CredentialsModal({
</Button> </Button>
</div> </div>
</div> </div>
<DialogFooter className="flex gap-2"> <DialogFooter className="flex gap-2">
<Button <Button
type="button" type="button"
@@ -118,4 +119,4 @@ export function CredentialsModal({
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
} }

View File

@@ -1,5 +1,14 @@
import { Link, useLocation } from "wouter"; import { Link, useLocation } from "wouter";
import { LayoutDashboard, Users, Calendar, Settings, FileCheck, Shield, CreditCard, FolderOpen } from "lucide-react"; import {
LayoutDashboard,
Users,
Calendar,
Settings,
FileCheck,
Shield,
CreditCard,
FolderOpen,
} from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -62,29 +71,36 @@ export function Sidebar({ isMobileOpen, setIsMobileOpen }: SidebarProps) {
)} )}
> >
<div className="p-4 border-b border-gray-200 flex items-center space-x-2"> <div className="p-4 border-b border-gray-200 flex items-center space-x-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5 text-primary"> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-5 w-5 text-primary"
>
<path d="M12 14c-1.65 0-3-1.35-3-3V5c0-1.65 1.35-3 3-3s3 1.35 3 3v6c0 1.65-1.35 3-3 3Z" /> <path d="M12 14c-1.65 0-3-1.35-3-3V5c0-1.65 1.35-3 3-3s3 1.35 3 3v6c0 1.65-1.35 3-3 3Z" />
<path d="M19 14v-4a7 7 0 0 0-14 0v4" /> <path d="M19 14v-4a7 7 0 0 0-14 0v4" />
<path d="M12 19c-5 0-8-2-9-5.5m18 0c-1 3.5-4 5.5-9 5.5Z" /> <path d="M12 19c-5 0-8-2-9-5.5m18 0c-1 3.5-4 5.5-9 5.5Z" />
</svg> </svg>
<h1 className="text-lg font-medium text-primary">DentalConnect</h1> <h1 className="text-lg font-medium text-primary">DentalConnect</h1>
</div> </div>
<div className="p-2"> <div className="p-2">
<nav> <nav>
{navItems.map((item) => ( {navItems.map((item) => (
<div key={item.path}> <div key={item.path}>
<Link <Link to={item.path} onClick={() => setIsMobileOpen(false)}>
to={item.path} <div
onClick={() => setIsMobileOpen(false)} className={cn(
> "flex items-center space-x-3 p-2 rounded-md pl-3 mb-1 transition-colors cursor-pointer",
<div className={cn( location === item.path
"flex items-center space-x-3 p-2 rounded-md pl-3 mb-1 transition-colors cursor-pointer", ? "text-primary font-medium border-l-2 border-primary"
location === item.path : "text-gray-600 hover:bg-gray-100"
? "text-primary font-medium border-l-2 border-primary" )}
: "text-gray-600 hover:bg-gray-100" >
)}>
{item.icon} {item.icon}
<span>{item.name}</span> <span>{item.name}</span>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { Bell, Menu} from "lucide-react"; import { Bell, Menu } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
@@ -11,8 +11,6 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
interface TopAppBarProps { interface TopAppBarProps {
toggleMobileMenu: () => void; toggleMobileMenu: () => void;
} }
@@ -33,34 +31,39 @@ export function TopAppBar({ toggleMobileMenu }: TopAppBarProps) {
<header className="bg-white shadow-sm z-10"> <header className="bg-white shadow-sm z-10">
<div className="flex items-center justify-between h-16 px-4"> <div className="flex items-center justify-between h-16 px-4">
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="md:hidden mr-2" className="md:hidden mr-2"
onClick={toggleMobileMenu} onClick={toggleMobileMenu}
> >
<Menu className="h-5 w-5" /> <Menu className="h-5 w-5" />
</Button> </Button>
<h1 className="md:hidden text-lg font-medium text-primary">DentalConnect</h1> <h1 className="md:hidden text-lg font-medium text-primary">
DentalConnect
</h1>
</div> </div>
<div className="hidden md:flex md:flex-1 items-center justify-center"> <div className="hidden md:flex md:flex-1 items-center justify-center">
{/* Search bar removed */} {/* Search bar removed */}
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="relative p-0 h-9 w-9 rounded-full" className="relative p-0 h-9 w-9 rounded-full"
> >
<Bell className="h-5 w-5" /> <Bell className="h-5 w-5" />
<span className="absolute top-0 right-0 w-3 h-3 bg-red-500 rounded-full border-2 border-white"></span> <span className="absolute top-0 right-0 w-3 h-3 bg-red-500 rounded-full border-2 border-white"></span>
</Button> </Button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative p-0 h-8 w-8 rounded-full"> <Button
variant="ghost"
className="relative p-0 h-8 w-8 rounded-full"
>
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
<AvatarImage src="" alt={user?.username} /> <AvatarImage src="" alt={user?.username} />
<AvatarFallback className="bg-primary text-white"> <AvatarFallback className="bg-primary text-white">
@@ -72,8 +75,9 @@ export function TopAppBar({ toggleMobileMenu }: TopAppBarProps) {
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem>{user?.username}</DropdownMenuItem> <DropdownMenuItem>{user?.username}</DropdownMenuItem>
<DropdownMenuItem>My Profile</DropdownMenuItem> <DropdownMenuItem>My Profile</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLocation("/settings")}> <DropdownMenuItem onClick={() => setLocation("/settings")}>
Account Settings</DropdownMenuItem> Account Settings
</DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}> <DropdownMenuItem onClick={handleLogout}>
Log out Log out

View File

@@ -14,39 +14,10 @@ import {
DialogContent, DialogContent,
DialogFooter, DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { PatientForm, PatientFormRef } from "./patient-form"; import { PatientForm, PatientFormRef } from "./patient-form";
import { useToast } from "@/hooks/use-toast";
import { X, Calendar } from "lucide-react"; import { X, Calendar } from "lucide-react";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; import { InsertPatient, Patient, UpdatePatient } from "@repo/db/types";
import { z } from "zod";
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>;
interface AddPatientModalProps { interface AddPatientModalProps {
open: boolean; open: boolean;
@@ -108,12 +79,11 @@ export const AddPatientModal = forwardRef<
}; };
const handleSaveAndSchedule = () => { const handleSaveAndSchedule = () => {
setSaveAndSchedule(true); setSaveAndSchedule(true);
if (patientFormRef.current) { if (patientFormRef.current) {
patientFormRef.current.submit(); patientFormRef.current.submit();
} }
}; };
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>

View File

@@ -1,7 +1,5 @@
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { import {
Form, Form,
@@ -32,34 +30,14 @@ import { format } from "date-fns";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
import {
const PatientSchema = ( InsertPatient,
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> insertPatientSchema,
).omit({ Patient,
appointments: true, UpdatePatient,
}); updatePatientSchema,
type Patient = z.infer<typeof PatientSchema>; } from "@repo/db/types";
import { z } from "zod";
const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
userId: true,
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>;
interface PatientFormProps { interface PatientFormProps {
patient?: Patient; patient?: Patient;

View File

@@ -85,25 +85,25 @@ export function PatientSearch({
<div className="relative flex-1"> <div className="relative flex-1">
{searchBy === "dob" ? ( {searchBy === "dob" ? (
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") handleSearch(); if (e.key === "Enter") handleSearch();
}} }}
className={cn( className={cn(
"w-full pl-3 pr-20 text-left font-normal", "w-full pl-3 pr-20 text-left font-normal",
!searchTerm && "text-muted-foreground" !searchTerm && "text-muted-foreground"
)} )}
> >
{searchTerm ? ( {searchTerm ? (
format(new Date(searchTerm), "PPP") format(new Date(searchTerm), "PPP")
) : ( ) : (
<span>Pick a date</span> <span>Pick a date</span>
)} )}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-4"> <PopoverContent className="w-auto p-4">
<Calendar <Calendar
mode="single" mode="single"

View File

@@ -18,8 +18,6 @@ import {
PaginationNext, PaginationNext,
PaginationPrevious, PaginationPrevious,
} from "@/components/ui/pagination"; } from "@/components/ui/pagination";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import { z } from "zod";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient } from "@/lib/queryClient";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import LoadingScreen from "../ui/LoadingScreen"; import LoadingScreen from "../ui/LoadingScreen";
@@ -39,25 +37,7 @@ import { useDebounce } from "use-debounce";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Checkbox } from "../ui/checkbox"; import { Checkbox } from "../ui/checkbox";
import { formatDateToHumanReadable } from "@/utils/dateUtils"; import { formatDateToHumanReadable } from "@/utils/dateUtils";
import { Patient, UpdatePatient } from "@repo/db/types";
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>;
interface PatientApiResponse { interface PatientApiResponse {
patients: Patient[]; patients: Patient[];
@@ -114,7 +94,7 @@ export function PatientTable({
const handleSelectPatient = (patient: Patient) => { const handleSelectPatient = (patient: Patient) => {
const isSelected = selectedPatientId === patient.id; const isSelected = selectedPatientId === patient.id;
const newSelectedId = isSelected ? null : patient.id; const newSelectedId = isSelected ? null : patient.id;
setSelectedPatientId(newSelectedId); setSelectedPatientId(Number(newSelectedId));
if (onSelectPatient) { if (onSelectPatient) {
onSelectPatient(isSelected ? null : patient); onSelectPatient(isSelected ? null : patient);
@@ -258,7 +238,7 @@ export function PatientTable({
if (currentPatient && user) { if (currentPatient && user) {
const { id, ...sanitizedPatient } = patient; const { id, ...sanitizedPatient } = patient;
updatePatientMutation.mutate({ updatePatientMutation.mutate({
id: currentPatient.id, id: Number(currentPatient.id),
patient: sanitizedPatient, patient: sanitizedPatient,
}); });
} else { } else {
@@ -288,7 +268,7 @@ export function PatientTable({
const handleConfirmDeletePatient = async () => { const handleConfirmDeletePatient = async () => {
if (currentPatient) { if (currentPatient) {
deletePatientMutation.mutate(currentPatient.id); deletePatientMutation.mutate(Number(currentPatient.id));
} else { } else {
toast({ toast({
title: "Error", title: "Error",
@@ -424,7 +404,7 @@ export function PatientTable({
<TableCell> <TableCell>
<div className="flex items-center"> <div className="flex items-center">
<Avatar <Avatar
className={`h-10 w-10 ${getAvatarColor(patient.id)}`} className={`h-10 w-10 ${getAvatarColor(Number(patient.id))}`}
> >
<AvatarFallback className="text-white"> <AvatarFallback className="text-white">
{getInitials(patient.firstName, patient.lastName)} {getInitials(patient.firstName, patient.lastName)}
@@ -436,7 +416,7 @@ export function PatientTable({
{patient.firstName} {patient.lastName} {patient.firstName} {patient.lastName}
</div> </div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
PID-{patient.id.toString().padStart(4, "0")} PID-{patient.id?.toString().padStart(4, "0")}
</div> </div>
</div> </div>
</div> </div>
@@ -505,16 +485,16 @@ export function PatientTable({
</Button> </Button>
)} )}
{allowNewClaim && ( {allowNewClaim && (
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => onNewClaim?.(patient.id)} onClick={() => onNewClaim?.(Number(patient.id))}
className="text-green-600 hover:text-green-800 hover:bg-green-50" className="text-green-600 hover:text-green-800 hover:bg-green-50"
aria-label="New Claim" aria-label="New Claim"
> >
<FileCheck className="h-5 w-5" /> <FileCheck className="h-5 w-5" />
</Button> </Button>
)} )}
{allowView && ( {allowView && (
<Button <Button
variant="ghost" variant="ghost"
@@ -558,7 +538,7 @@ export function PatientTable({
{currentPatient.firstName} {currentPatient.lastName} {currentPatient.firstName} {currentPatient.lastName}
</h3> </h3>
<p className="text-gray-500"> <p className="text-gray-500">
Patient ID: {currentPatient.id.toString().padStart(4, "0")} Patient ID: {currentPatient.id?.toString().padStart(4, "0")}
</p> </p>
</div> </div>
</div> </div>
@@ -587,8 +567,10 @@ export function PatientTable({
: "text-red-600" : "text-red-600"
} font-medium`} } font-medium`}
> >
{currentPatient.status.charAt(0).toUpperCase() + {currentPatient.status
currentPatient.status.slice(1)} ? currentPatient.status.charAt(0).toUpperCase() +
currentPatient.status.slice(1)
: "Unknown"}
</span> </span>
</p> </p>
</div> </div>
@@ -706,7 +688,7 @@ export function PatientTable({
isOpen={isDeletePatientOpen} isOpen={isDeletePatientOpen}
onConfirm={handleConfirmDeletePatient} onConfirm={handleConfirmDeletePatient}
onCancel={() => setIsDeletePatientOpen(false)} onCancel={() => setIsDeletePatientOpen(false)}
entityName={currentPatient?.name} entityName={currentPatient?.firstName}
/> />
{/* Pagination */} {/* Pagination */}
@@ -731,25 +713,25 @@ export function PatientTable({
} }
/> />
</PaginationItem> </PaginationItem>
{getPageNumbers(currentPage, totalPages).map((page, idx) => ( {getPageNumbers(currentPage, totalPages).map((page, idx) => (
<PaginationItem key={idx}> <PaginationItem key={idx}>
{page === "..." ? ( {page === "..." ? (
<span className="px-2 text-gray-500">...</span> <span className="px-2 text-gray-500">...</span>
) : ( ) : (
<PaginationLink <PaginationLink
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setCurrentPage(page as number); setCurrentPage(page as number);
}} }}
isActive={currentPage === page} isActive={currentPage === page}
> >
{page} {page}
</PaginationLink> </PaginationLink>
)} )}
</PaginationItem> </PaginationItem>
))} ))}
<PaginationItem> <PaginationItem>
<PaginationNext <PaginationNext

View File

@@ -6,11 +6,20 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { formatDateToHumanReadable } from "@/utils/dateUtils"; import {
formatDateToHumanReadable,
formatLocalDate,
parseLocalDate,
} from "@/utils/dateUtils";
import React, { useState } from "react"; import React, { useState } from "react";
import { paymentStatusOptions, PaymentWithExtras } from "@repo/db/types"; import {
import { PaymentStatus, PaymentMethod } from "@repo/db/types"; PaymentStatus,
paymentStatusOptions,
PaymentMethod,
paymentMethodOptions,
PaymentWithExtras,
NewTransactionPayload,
} from "@repo/db/types";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -26,7 +35,7 @@ type PaymentEditModalProps = {
isOpen: boolean; isOpen: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onClose: () => void; onClose: () => void;
onEditServiceLine: (updatedPayment: PaymentWithExtras) => void; onEditServiceLine: (payload: NewTransactionPayload) => void;
payment: PaymentWithExtras | null; payment: PaymentWithExtras | null;
}; };
@@ -40,143 +49,88 @@ export default function PaymentEditModal({
if (!payment) return null; if (!payment) return null;
const [expandedLineId, setExpandedLineId] = useState<number | null>(null); const [expandedLineId, setExpandedLineId] = useState<number | null>(null);
const [updatedPaidAmounts, setUpdatedPaidAmounts] = useState< const [paymentStatus, setPaymentStatus] = React.useState<PaymentStatus>(
Record<number, number> payment.status
>({});
const [updatedAdjustedAmounts, setUpdatedAdjustedAmounts] = useState<
Record<number, number>
>({});
const [updatedNotes, setUpdatedNotes] = useState<Record<number, string>>({});
const [updatedPaymentStatus, setUpdatedPaymentStatus] =
useState<PaymentStatus>(payment?.status ?? "PENDING");
const [updatedTransactions, setUpdatedTransactions] = useState(
() => payment?.transactions ?? []
); );
const [formState, setFormState] = useState(() => {
type DraftPaymentData = { return {
paidAmount: number; serviceLineId: 0,
adjustedAmount?: number; transactionId: "",
notes?: string; paidAmount: 0,
paymentStatus?: PaymentStatus; adjustedAmount: 0,
payerName?: string; method: paymentMethodOptions[1] as PaymentMethod,
method: PaymentMethod; receivedDate: formatLocalDate(new Date()),
receivedDate: string; payerName: "",
}; notes: "",
};
});
const [serviceLineDrafts, setServiceLineDrafts] = useState<Record<number, any>>({});
const totalPaid = payment.transactions.reduce(
(sum, tx) =>
sum +
tx.serviceLinePayments.reduce((s, sp) => s + Number(sp.paidAmount), 0),
0
);
const totalBilled = payment.claim.serviceLines.reduce(
(sum, line) => sum + line.billedAmount,
0
);
const totalDue = totalBilled - totalPaid;
const handleEditServiceLine = (lineId: number) => { const handleEditServiceLine = (lineId: number) => {
setExpandedLineId(lineId === expandedLineId ? null : lineId); if (expandedLineId === lineId) {
}; // Closing current line
setExpandedLineId(null);
const handleFieldChange = (lineId: number, field: string, value: any) => {
setServiceLineDrafts((prev) => ({
...prev,
[lineId]: {
...prev[lineId],
[field]: value,
},
}));
};
// const handleSavePayment = (lineId: number) => {
// const newPaidAmount = updatedPaidAmounts[lineId];
// const newAdjustedAmount = updatedAdjustedAmounts[lineId] ?? 0;
// const newNotes = updatedNotes[lineId] ?? "";
// if (newPaidAmount == null || isNaN(newPaidAmount)) return;
// const updatedTxs = updatedTransactions.map((tx) => ({
// ...tx,
// serviceLinePayments: tx.serviceLinePayments.map((sp) =>
// sp.serviceLineId === lineId
// ? {
// ...sp,
// paidAmount: new Decimal(newPaidAmount),
// adjustedAmount: new Decimal(newAdjustedAmount),
// notes: newNotes,
// }
// : sp
// ),
// }));
// const updatedPayment: PaymentWithExtras = {
// ...payment,
// transactions: updatedTxs,
// status: updatedPaymentStatus,
// };
// setUpdatedTransactions(updatedTxs);
// onEditServiceLine(updatedPayment);
// setExpandedLineId(null);
// };
const handleSavePayment = async (lineId: number) => {
const data = serviceLineDrafts[lineId];
if (!data || !data.paidAmount || !data.method || !data.receivedDate) {
console.log("please fill al")
return; return;
} }
const transactionPayload = { // Find line data
const line = payment.claim.serviceLines.find((sl) => sl.id === lineId);
if (!line) return;
// updating form to show its data, while expanding.
setFormState({
serviceLineId: line.id,
transactionId: "",
paidAmount: Number(line.totalDue) > 0 ? Number(line.totalDue) : 0,
adjustedAmount: 0,
method: paymentMethodOptions[1] as PaymentMethod,
receivedDate: formatLocalDate(new Date()),
payerName: "",
notes: "",
});
setExpandedLineId(lineId);
};
const updateField = (field: string, value: any) => {
setFormState((prev) => ({
...prev,
[field]: value,
}));
};
const handleSavePayment = async () => {
if (!formState.serviceLineId) {
toast({ title: "Error", description: "No service line selected." });
return;
}
const payload: NewTransactionPayload = {
paymentId: payment.id, paymentId: payment.id,
amount: data.paidAmount + (data.adjustedAmount ?? 0), status: paymentStatus,
method: data.method, serviceLineTransactions: [
payerName: data.payerName,
notes: data.notes,
receivedDate: new Date(data.receivedDate),
serviceLinePayments: [
{ {
serviceLineId: lineId, serviceLineId: formState.serviceLineId,
paidAmount: data.paidAmount, transactionId: formState.transactionId || undefined,
adjustedAmount: data.adjustedAmount ?? 0, paidAmount: Number(formState.paidAmount),
notes: data.notes, adjustedAmount: Number(formState.adjustedAmount) || 0,
method: formState.method,
receivedDate: parseLocalDate(formState.receivedDate),
payerName: formState.payerName || undefined,
notes: formState.notes || undefined,
}, },
], ],
}; };
try { try {
await onEditServiceLine(transactionPayload); await onEditServiceLine(payload);
setExpandedLineId(null); setExpandedLineId(null);
onClose(); onClose();
} catch (err) { } catch (err) {
console.log(err) console.error(err);
toast({ title: "Error", description: "Failed to save payment." });
} }
}; };
const renderInput = (label: string, type: string, lineId: number, field: string, step?: string) => (
<div className="space-y-1">
<label className="text-sm font-medium">{label}</label>
<Input
type={type}
step={step}
value={serviceLineDrafts[lineId]?.[field] ?? ""}
onChange={(e) =>
handleFieldChange(lineId, field, type === "number" ? parseFloat(e.target.value) : e.target.value)
}
/>
</div>
);
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">
@@ -200,6 +154,10 @@ export default function PaymentEditModal({
Service Date:{" "} Service Date:{" "}
{formatDateToHumanReadable(payment.claim.serviceDate)} {formatDateToHumanReadable(payment.claim.serviceDate)}
</p> </p>
<p>
<span className="text-gray-500">Notes:</span>{" "}
{payment.notes || "N/A"}
</p>
</div> </div>
{/* Payment Summary */} {/* Payment Summary */}
@@ -209,22 +167,22 @@ export default function PaymentEditModal({
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
<p> <p>
<span className="text-gray-500">Total Billed:</span> $ <span className="text-gray-500">Total Billed:</span> $
{totalBilled.toFixed(2)} {payment.totalBilled.toNumber().toFixed(2)}
</p> </p>
<p> <p>
<span className="text-gray-500">Total Paid:</span> $ <span className="text-gray-500">Total Paid:</span> $
{totalPaid.toFixed(2)} {payment.totalPaid.toNumber().toFixed(2)}
</p> </p>
<p> <p>
<span className="text-gray-500">Total Due:</span> $ <span className="text-gray-500">Total Due:</span> $
{totalDue.toFixed(2)} {payment.totalDue.toNumber().toFixed(2)}
</p> </p>
<div className="pt-2"> <div className="pt-2">
<label className="text-sm text-gray-600">Status</label> <label className="text-sm text-gray-600">Status</label>
<Select <Select
value={updatedPaymentStatus} value={paymentStatus}
onValueChange={(value: PaymentStatus) => onValueChange={(value: PaymentStatus) =>
setUpdatedPaymentStatus(value) setPaymentStatus(value)
} }
> >
<SelectTrigger> <SelectTrigger>
@@ -246,18 +204,16 @@ export default function PaymentEditModal({
<h4 className="font-medium text-gray-900">Metadata</h4> <h4 className="font-medium text-gray-900">Metadata</h4>
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
<p> <p>
<span className="text-gray-500">Received Date:</span>{" "} <span className="text-gray-500">Created At:</span>{" "}
{payment.receivedDate {payment.createdAt
? formatDateToHumanReadable(payment.receivedDate) ? formatDateToHumanReadable(payment.createdAt)
: "N/A"} : "N/A"}
</p> </p>
<p> <p>
<span className="text-gray-500">Method:</span>{" "} <span className="text-gray-500">Last Upadated At:</span>{" "}
{payment.paymentMethod ?? "N/A"} {payment.updatedAt
</p> ? formatDateToHumanReadable(payment.updatedAt)
<p> : "N/A"}
<span className="text-gray-500">Notes:</span>{" "}
{payment.notes || "N/A"}
</p> </p>
</div> </div>
</div> </div>
@@ -270,22 +226,6 @@ export default function PaymentEditModal({
{payment.claim.serviceLines.length > 0 ? ( {payment.claim.serviceLines.length > 0 ? (
<> <>
{payment.claim.serviceLines.map((line) => { {payment.claim.serviceLines.map((line) => {
const linePayments = payment.transactions.flatMap((tx) =>
tx.serviceLinePayments.filter(
(sp) => sp.serviceLineId === line.id
)
);
const paidAmount = linePayments.reduce(
(sum, sp) => sum + Number(sp.paidAmount),
0
);
const adjusted = linePayments.reduce(
(sum, sp) => sum + Number(sp.adjustedAmount),
0
);
const due = line.billedAmount - paidAmount;
return ( return (
<div <div
key={line.id} key={line.id}
@@ -297,19 +237,19 @@ export default function PaymentEditModal({
</p> </p>
<p> <p>
<span className="text-gray-500">Billed:</span> $ <span className="text-gray-500">Billed:</span> $
{line.billedAmount.toFixed(2)} {line.totalBilled.toFixed(2)}
</p> </p>
<p> <p>
<span className="text-gray-500">Paid:</span> $ <span className="text-gray-500">Paid:</span> $
{paidAmount.toFixed(2)} {line.totalPaid.toFixed(2)}
</p> </p>
<p> <p>
<span className="text-gray-500">Adjusted:</span> $ <span className="text-gray-500">Adjusted:</span> $
{adjusted.toFixed(2)} {line.totalAdjusted.toFixed(2)}
</p> </p>
<p> <p>
<span className="text-gray-500">Due:</span> $ <span className="text-gray-500">Due:</span> $
{due.toFixed(2)} {line.totalDue.toFixed(2)}
</p> </p>
<div className="pt-2"> <div className="pt-2">
@@ -337,12 +277,12 @@ export default function PaymentEditModal({
type="number" type="number"
step="0.01" step="0.01"
placeholder="Paid Amount" placeholder="Paid Amount"
defaultValue={paidAmount} defaultValue={formState.paidAmount}
onChange={(e) => onChange={(e) =>
setUpdatedPaidAmounts({ updateField(
...updatedPaidAmounts, "paidAmount",
[line.id]: parseFloat(e.target.value), parseFloat(e.target.value)
}) )
} }
/> />
</div> </div>
@@ -358,12 +298,65 @@ export default function PaymentEditModal({
type="number" type="number"
step="0.01" step="0.01"
placeholder="Adjusted Amount" placeholder="Adjusted Amount"
defaultValue={adjusted} defaultValue={formState.adjustedAmount}
onChange={(e) => onChange={(e) =>
setUpdatedAdjustedAmounts({ updateField(
...updatedAdjustedAmounts, "adjustedAmount",
[line.id]: parseFloat(e.target.value), parseFloat(e.target.value)
}) )
}
/>
</div>
<div className="space-y-1">
<label className="text-sm font-medium">
Payment Method
</label>
<Select
value={formState.method}
onValueChange={(value: PaymentMethod) =>
updateField("method", value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{paymentMethodOptions.map((methodOption) => (
<SelectItem
key={methodOption}
value={methodOption}
>
{methodOption}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<label className="text-sm font-medium">
Received Date
</label>
<Input
type="date"
value={formState.receivedDate}
onChange={(e) =>
updateField("receivedDate", e.target.value)
}
/>
</div>
<div className="space-y-1">
<label className="text-sm font-medium">
Payer Name
</label>
<Input
type="text"
placeholder="Payer Name"
value={formState.payerName}
onChange={(e) =>
updateField("payerName", e.target.value)
} }
/> />
</div> </div>
@@ -380,16 +373,13 @@ export default function PaymentEditModal({
type="text" type="text"
placeholder="Notes" placeholder="Notes"
onChange={(e) => onChange={(e) =>
setUpdatedNotes({ updateField("notes", e.target.value)
...updatedNotes,
[line.id]: e.target.value,
})
} }
/> />
</div> </div>
<Button <Button
size="sm" size="sm"
onClick={() => handleSavePayment(line.id)} onClick={() => handleSavePayment()}
> >
Save Save
</Button> </Button>
@@ -409,8 +399,8 @@ export default function PaymentEditModal({
<div> <div>
<h4 className="font-medium text-gray-900 pt-6">All Transactions</h4> <h4 className="font-medium text-gray-900 pt-6">All Transactions</h4>
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
{payment.transactions.length > 0 ? ( {payment.serviceLineTransactions.length > 0 ? (
payment.transactions.map((tx) => ( payment.serviceLineTransactions.map((tx) => (
<div <div
key={tx.id} key={tx.id}
className="border p-3 rounded-md bg-white shadow-sm" className="border p-3 rounded-md bg-white shadow-sm"
@@ -420,18 +410,27 @@ export default function PaymentEditModal({
{formatDateToHumanReadable(tx.receivedDate)} {formatDateToHumanReadable(tx.receivedDate)}
</p> </p>
<p> <p>
<span className="text-gray-500">Amount:</span> $ <span className="text-gray-500">Paid Amount:</span> $
{Number(tx.amount).toFixed(2)} {Number(tx.paidAmount).toFixed(2)}
</p>
<p>
<span className="text-gray-500">Adjusted Amount:</span> $
{Number(tx.adjustedAmount).toFixed(2)}
</p> </p>
<p> <p>
<span className="text-gray-500">Method:</span> {tx.method} <span className="text-gray-500">Method:</span> {tx.method}
</p> </p>
{tx.serviceLinePayments.map((sp) => ( {tx.payerName && (
<p key={sp.id} className="text-sm text-gray-600 ml-2"> <p>
Applied ${Number(sp.paidAmount).toFixed(2)} to service <span className="text-gray-500">Payer Name:</span>{" "}
line ID {sp.serviceLineId} {tx.payerName}
</p> </p>
))} )}
{tx.notes && (
<p>
<span className="text-gray-500">Notes:</span> {tx.notes}
</p>
)}
</div> </div>
)) ))
) : ( ) : (

View File

@@ -31,12 +31,11 @@ import {
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { DeleteConfirmationDialog } from "../ui/deleteDialog"; import { DeleteConfirmationDialog } from "../ui/deleteDialog";
import PaymentViewModal from "./payment-view-modal"; import PaymentViewModal from "./payment-view-modal";
import PaymentEditModal from "./payment-edit-modal";
import LoadingScreen from "../ui/LoadingScreen"; import LoadingScreen from "../ui/LoadingScreen";
import { import {
ClaimStatus, ClaimStatus,
ClaimWithServiceLines, ClaimWithServiceLines,
Payment, NewTransactionPayload,
PaymentWithExtras, PaymentWithExtras,
} from "@repo/db/types"; } from "@repo/db/types";
import EditPaymentModal from "./payment-edit-modal"; import EditPaymentModal from "./payment-edit-modal";
@@ -118,10 +117,14 @@ export default function PaymentsRecentTable({
}); });
const updatePaymentMutation = useMutation({ const updatePaymentMutation = useMutation({
mutationFn: async (payment: PaymentWithExtras) => { mutationFn: async (data: NewTransactionPayload) => {
const response = await apiRequest("PUT", `/api/claims/${payment.id}`, { const response = await apiRequest(
data: payment, "PUT",
}); `/api/claims/${data.paymentId}`,
{
data: data,
}
);
if (!response.ok) { if (!response.ok) {
const error = await response.json(); const error = await response.json();
throw new Error(error.message || "Failed to update Payment"); throw new Error(error.message || "Failed to update Payment");
@@ -288,13 +291,6 @@ export default function PaymentsRecentTable({
} }
}; };
const getTotalBilled = (claim: ClaimWithServiceLines) => {
return claim.serviceLines.reduce(
(sum, line) => sum + (line.billedAmount || 0),
0
);
};
function getPageNumbers(current: number, total: number): (number | "...")[] { function getPageNumbers(current: number, total: number): (number | "...")[] {
const delta = 2; const delta = 2;
const range: (number | "...")[] = []; const range: (number | "...")[] = [];
@@ -359,19 +355,9 @@ export default function PaymentsRecentTable({
</TableRow> </TableRow>
) : ( ) : (
paymentsData?.payments.map((payment) => { paymentsData?.payments.map((payment) => {
const claim = (payment as PaymentWithExtras) const totalBilled = payment.totalBilled.toNumber();
.claim as ClaimWithServiceLines; const totalPaid = payment.totalPaid.toNumber();
const totalDue = payment.totalDue.toNumber();
const totalBilled = getTotalBilled(claim);
const totalPaid = (payment as PaymentWithExtras).transactions
.flatMap((tx) => tx.serviceLinePayments)
.reduce(
(sum, sp) => sum + (sp.paidAmount?.toNumber?.() ?? 0),
0
);
const outstanding = totalBilled - totalPaid;
return ( return (
<TableRow key={payment.id}> <TableRow key={payment.id}>
@@ -401,9 +387,9 @@ export default function PaymentsRecentTable({
</span> </span>
<span> <span>
<strong>Total Due:</strong>{" "} <strong>Total Due:</strong>{" "}
{outstanding > 0 ? ( {totalDue > 0 ? (
<span className="text-yellow-600"> <span className="text-yellow-600">
${outstanding.toFixed(2)} ${totalDue.toFixed(2)}
</span> </span>
) : ( ) : (
<span className="text-green-600">Settled</span> <span className="text-green-600">Settled</span>

View File

@@ -1,8 +1,5 @@
import { Staff } from "@repo/db/types";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import { z } from "zod";
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
interface StaffFormProps { interface StaffFormProps {
initialData?: Partial<Staff>; initialData?: Partial<Staff>;

View File

@@ -1,15 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { z } from "zod";
import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Delete, Edit } from "lucide-react"; import { Delete, Edit } from "lucide-react";
import { Staff } from "@repo/db/types";
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
const staffCreateSchema = StaffUncheckedCreateInputObjectSchema;
const staffUpdateSchema = (
StaffUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).partial();
interface StaffTableProps { interface StaffTableProps {
staff: Staff[]; staff: Staff[];
@@ -21,7 +13,6 @@ interface StaffTableProps {
onView: (staff: Staff) => void; onView: (staff: Staff) => void;
} }
export function StaffTable({ export function StaffTable({
staff, staff,
onEdit, onEdit,
@@ -151,15 +142,13 @@ export function StaffTable({
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2"> <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
<Button <Button
onClick={() => onClick={() => staff !== undefined && onDelete(staff)}
staff !== undefined && onDelete(staff)
}
className="text-red-600 hover:text-red-900" className="text-red-600 hover:text-red-900"
aria-label="Delete Staff" aria-label="Delete Staff"
variant="ghost" variant="ghost"
size="icon" size="icon"
> >
<Delete/> <Delete />
</Button> </Button>
<Button <Button
onClick={() => staff.id !== undefined && onEdit(staff)} onClick={() => staff.id !== undefined && onEdit(staff)}
@@ -170,7 +159,6 @@ export function StaffTable({
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
</td> </td>
</tr> </tr>
); );
@@ -235,7 +223,9 @@ export function StaffTable({
if (currentPage < totalPages) setCurrentPage(currentPage + 1); if (currentPage < totalPages) setCurrentPage(currentPage + 1);
}} }}
className={`relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 ${ className={`relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 ${
currentPage === totalPages ? "pointer-events-none opacity-50" : "" currentPage === totalPages
? "pointer-events-none opacity-50"
: ""
}`} }`}
> >
Next Next

View File

@@ -1,10 +1,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useQuery, useMutation } from "@tanstack/react-query"; import { useQuery, useMutation } from "@tanstack/react-query";
import { format, addDays, startOfToday, addMinutes } from "date-fns"; import { format, addDays, startOfToday, addMinutes } from "date-fns";
import { import { parseLocalDate, formatLocalDate } from "@/utils/dateUtils";
parseLocalDate,
formatLocalDate,
} from "@/utils/dateUtils";
import { TopAppBar } from "@/components/layout/top-app-bar"; import { TopAppBar } from "@/components/layout/top-app-bar";
import { Sidebar } from "@/components/layout/sidebar"; import { Sidebar } from "@/components/layout/sidebar";
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal"; import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
@@ -19,12 +16,7 @@ import {
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { z } from "zod";
import { Calendar } from "@/components/ui/calendar"; import { Calendar } from "@/components/ui/calendar";
import {
AppointmentUncheckedCreateInputObjectSchema,
PatientUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient } from "@/lib/queryClient";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { import {
@@ -40,35 +32,12 @@ import { Menu, Item, useContextMenu } from "react-contexify";
import "react-contexify/ReactContexify.css"; import "react-contexify/ReactContexify.css";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog"; import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
import {
//creating types out of schema auto generated. Appointment,
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>; InsertAppointment,
Patient,
const insertAppointmentSchema = ( UpdateAppointment,
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> } from "@repo/db/types";
).omit({
id: true,
createdAt: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
// Define types for scheduling // Define types for scheduling
interface TimeSlot { interface TimeSlot {
@@ -287,7 +256,11 @@ export default function AppointmentsPage() {
// Create/upsert appointment mutation // Create/upsert appointment mutation
const createAppointmentMutation = useMutation({ const createAppointmentMutation = useMutation({
mutationFn: async (appointment: InsertAppointment) => { mutationFn: async (appointment: InsertAppointment) => {
const res = await apiRequest("POST", "/api/appointments/upsert", appointment); const res = await apiRequest(
"POST",
"/api/appointments/upsert",
appointment
);
return await res.json(); return await res.json();
}, },
onSuccess: () => { onSuccess: () => {
@@ -436,8 +409,8 @@ export default function AppointmentsPage() {
const formattedDate = format(selectedDate, "yyyy-MM-dd"); const formattedDate = format(selectedDate, "yyyy-MM-dd");
const selectedDateAppointments = appointments.filter((appointment) => { const selectedDateAppointments = appointments.filter((appointment) => {
const dateObj = parseLocalDate(appointment.date) const dateObj = parseLocalDate(appointment.date);
return formatLocalDate(dateObj) === formatLocalDate(selectedDate); return formatLocalDate(dateObj) === formatLocalDate(selectedDate);
}); });
// Process appointments for the scheduler view // Process appointments for the scheduler view
@@ -466,8 +439,8 @@ export default function AppointmentsPage() {
...apt, ...apt,
patientName, patientName,
staffId, staffId,
status: apt.status ?? null, status: apt.status ?? null,
date: formatLocalDate(parseLocalDate(apt.date)) date: formatLocalDate(parseLocalDate(apt.date)),
}; };
return processed; return processed;

View File

@@ -1,7 +1,5 @@
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { UserUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -16,41 +14,17 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Card} from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { CheckCircle, Torus } from "lucide-react"; import { CheckCircle, Torus } from "lucide-react";
import { CheckedState } from "@radix-ui/react-checkbox"; import { CheckedState } from "@radix-ui/react-checkbox";
import LoadingScreen from "@/components/ui/LoadingScreen"; import LoadingScreen from "@/components/ui/LoadingScreen";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import {
const insertUserSchema = ( LoginFormValues,
UserUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> loginSchema,
).pick({ RegisterFormValues,
username: true, registerSchema,
password: true, } from "@repo/db/types";
});
const loginSchema = (insertUserSchema as unknown as z.ZodObject<any>).extend({
rememberMe: z.boolean().optional(),
});
const registerSchema = (insertUserSchema as unknown as z.ZodObject<any>)
.extend({
confirmPassword: z.string().min(6, {
message: "Password must be at least 6 characters long",
}),
agreeTerms: z.literal(true, {
errorMap: () => ({
message: "You must agree to the terms and conditions",
}),
}),
})
.refine((data: any) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
type LoginFormValues = z.infer<typeof loginSchema>;
type RegisterFormValues = z.infer<typeof registerSchema>;
export default function AuthPage() { export default function AuthPage() {
const [activeTab, setActiveTab] = useState<string>("login"); const [activeTab, setActiveTab] = useState<string>("login");

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { useMutation} from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { TopAppBar } from "@/components/layout/top-app-bar"; import { TopAppBar } from "@/components/layout/top-app-bar";
import { Sidebar } from "@/components/layout/sidebar"; import { Sidebar } from "@/components/layout/sidebar";
import { import {
@@ -12,13 +12,7 @@ import {
import { ClaimForm } from "@/components/claims/claim-form"; import { ClaimForm } from "@/components/claims/claim-form";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import {
PatientUncheckedCreateInputObjectSchema,
AppointmentUncheckedCreateInputObjectSchema,
ClaimUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import { parse } from "date-fns"; import { parse } from "date-fns";
import { z } from "zod";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient } from "@/lib/queryClient";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import { useAppDispatch, useAppSelector } from "@/redux/hooks";
@@ -27,58 +21,16 @@ import {
clearTaskStatus, clearTaskStatus,
} from "@/redux/slices/seleniumClaimSubmitTaskSlice"; } from "@/redux/slices/seleniumClaimSubmitTaskSlice";
import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner"; import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner";
import { formatLocalDate} from "@/utils/dateUtils"; import { formatLocalDate } from "@/utils/dateUtils";
import ClaimsRecentTable from "@/components/claims/claims-recent-table"; import ClaimsRecentTable from "@/components/claims/claims-recent-table";
import ClaimsOfPatientModal from "@/components/claims/claims-of-patient-table"; import ClaimsOfPatientModal from "@/components/claims/claims-of-patient-table";
import {
//creating types out of schema auto generated. Claim,
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>; InsertAppointment,
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>; InsertPatient,
UpdateAppointment,
const insertAppointmentSchema = ( UpdatePatient,
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> } from "@repo/db/types";
).omit({
id: true,
createdAt: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>;
export default function ClaimsPage() { export default function ClaimsPage() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);

View File

@@ -27,42 +27,13 @@ import {
import { Link } from "wouter"; import { Link } from "wouter";
import { z } from "zod"; import { z } from "zod";
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
import {
//creating types out of schema auto generated. Appointment,
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>; InsertAppointment,
InsertPatient,
const insertAppointmentSchema = ( Patient,
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> UpdateAppointment,
).omit({ } from "@repo/db/types";
id: true,
createdAt: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
// Type for the ref to access modal methods // Type for the ref to access modal methods
type AddPatientModalRef = { type AddPatientModalRef = {
@@ -156,7 +127,11 @@ export default function Dashboard() {
// Create/upsert appointment mutation // Create/upsert appointment mutation
const createAppointmentMutation = useMutation({ const createAppointmentMutation = useMutation({
mutationFn: async (appointment: InsertAppointment) => { mutationFn: async (appointment: InsertAppointment) => {
const res = await apiRequest("POST", "/api/appointments/upsert", appointment); const res = await apiRequest(
"POST",
"/api/appointments/upsert",
appointment
);
return await res.json(); return await res.json();
}, },
onSuccess: () => { onSuccess: () => {

View File

@@ -11,26 +11,11 @@ import { Button } from "@/components/ui/button";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/use-toast";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient } from "@/lib/queryClient";
import { Eye, Trash, Download, FolderOpen } from "lucide-react"; import { Eye, Trash, Download, FolderOpen } from "lucide-react";
import {
PatientUncheckedCreateInputObjectSchema,
PdfFileUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog"; import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
import { PatientTable } from "@/components/patients/patient-table"; import { PatientTable } from "@/components/patients/patient-table";
import { z } from "zod";
import { Sidebar } from "@/components/layout/sidebar"; import { Sidebar } from "@/components/layout/sidebar";
import { TopAppBar } from "@/components/layout/top-app-bar"; import { TopAppBar } from "@/components/layout/top-app-bar";
import { Patient, PdfFile } from "@repo/db/types";
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
const PdfFileSchema =
PdfFileUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>;
type PdfFile = z.infer<typeof PdfFileSchema>;
export default function DocumentsPage() { export default function DocumentsPage() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
@@ -44,11 +29,10 @@ export default function DocumentsPage() {
const toggleMobileMenu = () => setIsMobileMenuOpen((prev) => !prev); const toggleMobileMenu = () => setIsMobileMenuOpen((prev) => !prev);
useEffect(() => { useEffect(() => {
setSelectedGroupId(null); setSelectedGroupId(null);
setFileBlobUrl(null); setFileBlobUrl(null);
setSelectedPdfId(null); setSelectedPdfId(null);
}, [selectedPatient]); }, [selectedPatient]);
const { data: groups = [] } = useQuery({ const { data: groups = [] } = useQuery({
queryKey: ["groups", selectedPatient?.id], queryKey: ["groups", selectedPatient?.id],
@@ -99,7 +83,7 @@ export default function DocumentsPage() {
const handleConfirmDeletePdf = () => { const handleConfirmDeletePdf = () => {
if (currentPdf) { if (currentPdf) {
deletePdfMutation.mutate(currentPdf.id); deletePdfMutation.mutate(Number(currentPdf.id));
} else { } else {
toast({ toast({
title: "Error", title: "Error",

View File

@@ -13,10 +13,8 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { CalendarIcon, CheckCircle, LoaderCircleIcon } from "lucide-react"; import { CalendarIcon, CheckCircle, LoaderCircleIcon } from "lucide-react";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { z } from "zod";
import { PatientTable } from "@/components/patients/patient-table"; import { PatientTable } from "@/components/patients/patient-table";
import { format } from "date-fns"; import { format } from "date-fns";
import { Calendar } from "@/components/ui/calendar"; import { Calendar } from "@/components/ui/calendar";
@@ -34,22 +32,7 @@ import {
} from "@/redux/slices/seleniumEligibilityCheckTaskSlice"; } from "@/redux/slices/seleniumEligibilityCheckTaskSlice";
import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner"; import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner";
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
import { InsertPatient, Patient } from "@repo/db/types";
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
userId: true,
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
export default function InsuranceEligibilityPage() { export default function InsuranceEligibilityPage() {
const { user } = useAuth(); const { user } = useAuth();

View File

@@ -1,5 +1,5 @@
import { useState, useMemo, useRef } from "react"; import { useState, useRef } from "react";
import { useQuery, useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { TopAppBar } from "@/components/layout/top-app-bar"; import { TopAppBar } from "@/components/layout/top-app-bar";
import { Sidebar } from "@/components/layout/sidebar"; import { Sidebar } from "@/components/layout/sidebar";
import { PatientTable } from "@/components/patients/patient-table"; import { PatientTable } from "@/components/patients/patient-table";
@@ -15,28 +15,11 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient } from "@/lib/queryClient";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { z } from "zod";
import useExtractPdfData from "@/hooks/use-extractPdfData"; import useExtractPdfData from "@/hooks/use-extractPdfData";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { InsertPatient, Patient } from "@repo/db/types";
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
userId: true,
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
// Type for the ref to access modal methods // Type for the ref to access modal methods
type AddPatientModalRef = { type AddPatientModalRef = {
@@ -105,7 +88,6 @@ export default function PatientsPage() {
const isLoading = addPatientMutation.isPending; const isLoading = addPatientMutation.isPending;
// File upload handling // File upload handling
const handleFileUpload = (file: File) => { const handleFileUpload = (file: File) => {
setIsUploading(true); setIsUploading(true);

View File

@@ -13,15 +13,9 @@ import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { import {
CreditCard,
Clock,
CheckCircle,
AlertCircle, AlertCircle,
DollarSign, DollarSign,
Receipt,
Plus,
ArrowDown, ArrowDown,
ReceiptText,
Upload, Upload,
Image, Image,
X, X,
@@ -53,56 +47,7 @@ import {
DialogFooter, DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import PaymentsRecentTable from "@/components/payments/payments-recent-table"; import PaymentsRecentTable from "@/components/payments/payments-recent-table";
import { Appointment, Patient } from "@repo/db/types";
import {
AppointmentUncheckedCreateInputObjectSchema,
PatientUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import { z } from "zod";
//creating types out of schema auto generated.
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
const insertAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
//patient types
type Patient = z.infer<typeof PatientUncheckedCreateInputObjectSchema>;
const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
type InsertPatient = z.infer<typeof insertPatientSchema>;
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>;
export default function PaymentsPage() { export default function PaymentsPage() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);

View File

@@ -6,10 +6,9 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
// import { Patient, Appointment } from "@repo/db/shared/schemas";
import { Patient, Appointment } from "@repo/db/shared/schemas";
import { Plus, ClipboardCheck, Clock, CheckCircle, AlertCircle } from "lucide-react"; import { Plus, ClipboardCheck, Clock, CheckCircle, AlertCircle } from "lucide-react";
import { format } from "date-fns"; import { format } from "date-fns";
import { Appointment, Patient } from "@repo/db/types";
export default function PreAuthorizationsPage() { export default function PreAuthorizationsPage() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
@@ -138,7 +137,7 @@ export default function PreAuthorizationsPage() {
key={patient.id} key={patient.id}
className="py-4 flex items-center justify-between cursor-pointer hover:bg-gray-50" className="py-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
onClick={() => { onClick={() => {
setSelectedPatient(patient.id); setSelectedPatient(Number(patient.id));
handleNewPreAuth( handleNewPreAuth(
patient.id, patient.id,
dentalProcedures[Math.floor(Math.random() * 3)].name dentalProcedures[Math.floor(Math.random() * 3)].name

View File

@@ -5,16 +5,13 @@ import { Sidebar } from "@/components/layout/sidebar";
import { StaffTable } from "@/components/staffs/staff-table"; import { StaffTable } from "@/components/staffs/staff-table";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import { z } from "zod";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient } from "@/lib/queryClient";
import { StaffForm } from "@/components/staffs/staff-form"; import { StaffForm } from "@/components/staffs/staff-form";
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog"; import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
import { CredentialTable } from "@/components/settings/insuranceCredTable"; import { CredentialTable } from "@/components/settings/insuranceCredTable";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { Staff } from "@repo/db/types";
// Correctly infer Staff type from zod schema
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
export default function SettingsPage() { export default function SettingsPage() {
const { toast } = useToast(); const { toast } = useToast();

File diff suppressed because one or more lines are too long

View File

@@ -135,7 +135,7 @@ model ServiceLine {
oralCavityArea String? oralCavityArea String?
toothNumber String? toothNumber String?
toothSurface String? toothSurface String?
billedAmount Float totalBilled Decimal @db.Decimal(10, 2)
totalPaid Decimal @default(0.00) @db.Decimal(10, 2) totalPaid Decimal @default(0.00) @db.Decimal(10, 2)
totalAdjusted Decimal @default(0.00) @db.Decimal(10, 2) totalAdjusted Decimal @default(0.00) @db.Decimal(10, 2)
totalDue Decimal @default(0.00) @db.Decimal(10, 2) totalDue Decimal @default(0.00) @db.Decimal(10, 2)
@@ -212,9 +212,9 @@ model Payment {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade) claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade)
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
updatedBy User? @relation("PaymentUpdatedBy", fields: [updatedById], references: [id]) updatedBy User? @relation("PaymentUpdatedBy", fields: [updatedById], references: [id])
serviceLineTransactions ServiceLineTransaction[] serviceLineTransactions ServiceLineTransaction[]
@@index([id]) @@index([id])

View File

@@ -0,0 +1,22 @@
import { AppointmentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import {z} from "zod";
export type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
export const insertAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
export type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
export const updateAppointmentSchema = (
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
export type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;

View File

@@ -0,0 +1,59 @@
import { ClaimStatusSchema, ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import {z} from "zod";
import { Decimal } from "decimal.js";
import { Staff } from "@repo/db/types";
export const insertClaimSchema = (
ClaimUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export type InsertClaim = z.infer<typeof insertClaimSchema>;
export const updateClaimSchema = (
ClaimUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
updatedAt: true,
})
.partial();
export type UpdateClaim = z.infer<typeof updateClaimSchema>;
export type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
export type ClaimStatus = z.infer<typeof ClaimStatusSchema>;
//used in claim-form
export interface InputServiceLine {
procedureCode: string;
procedureDate: string; // YYYY-MM-DD
oralCavityArea?: string;
toothNumber?: string;
toothSurface?: string;
totalBilled: Decimal;
totalPaid?: Decimal;
totalAdjusted?: Decimal;
}
// Claim model with embedded service lines
export type ClaimWithServiceLines = Claim & {
serviceLines: {
id: number;
claimId: number;
procedureCode: string;
procedureDate: Date;
oralCavityArea: string | null;
toothNumber: string | null;
toothSurface: string | null;
totalBilled: Decimal;
totalPaid: Decimal;
totalAdjusted: Decimal;
status: string;
}[];
staff?: Staff | null;
};

View File

@@ -1,2 +1,8 @@
export * from "./appointment-types";
export * from "./claim-types";
export * from "./insurance-types";
export * from "./patient-types";;
export * from "./payment-types"; export * from "./payment-types";
export * from "./pdf-types";
export * from "./staff-types";
export * from "./user-types";

View File

@@ -0,0 +1,14 @@
import { InsuranceCredentialUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import {z} from "zod";
export type InsuranceCredential = z.infer<
typeof InsuranceCredentialUncheckedCreateInputObjectSchema
>;
export const insertInsuranceCredentialSchema = (
InsuranceCredentialUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({ id: true });
export type InsertInsuranceCredential = z.infer<
typeof insertInsuranceCredentialSchema
>;

View File

@@ -0,0 +1,24 @@
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import {z} from "zod";
export type Patient = z.infer<typeof PatientUncheckedCreateInputObjectSchema>;
export const insertPatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
export type InsertPatient = z.infer<typeof insertPatientSchema>;
export const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
export type UpdatePatient = z.infer<typeof updatePatientSchema>;

View File

@@ -91,38 +91,18 @@ export type PaymentWithExtras = Prisma.PaymentGetPayload<{
paymentMethod: string; paymentMethod: string;
}; };
// Claim model with embedded service lines
export type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
export type ClaimStatus = z.infer<typeof ClaimStatusSchema>;
export type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
export type ClaimWithServiceLines = Claim & {
serviceLines: {
id: number;
claimId: number;
procedureCode: string;
procedureDate: Date;
oralCavityArea: string | null;
toothNumber: string | null;
toothSurface: string | null;
billedAmount: number;
status: string;
}[];
staff: Staff | null;
};
export type NewTransactionPayload = { export type NewTransactionPayload = {
paymentId: number; paymentId: number;
amount: number; status: PaymentStatus;
method: PaymentMethod;
payerName?: string;
notes?: string;
receivedDate: Date;
serviceLineTransactions: { serviceLineTransactions: {
serviceLineId: number; serviceLineId: number;
transactionId?: string;
paidAmount: number; paidAmount: number;
adjustedAmount: number; adjustedAmount?: number;
method: PaymentMethod;
receivedDate: Date;
payerName?: string;
notes?: string; notes?: string;
}[]; }[];
}; };

View File

@@ -0,0 +1,12 @@
import { PdfCategorySchema, PdfFileUncheckedCreateInputObjectSchema, PdfGroupUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import {z} from "zod";
export type PdfGroup = z.infer<typeof PdfGroupUncheckedCreateInputObjectSchema>;
export type PdfFile = z.infer<typeof PdfFileUncheckedCreateInputObjectSchema>;
export type PdfCategory = z.infer<typeof PdfCategorySchema>;
export interface ClaimPdfMetadata {
id: number;
filename: string;
uploadedAt: Date;
}

View File

@@ -0,0 +1,4 @@
import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import {z} from "zod";
export type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;

View File

@@ -0,0 +1,36 @@
import { UserUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import {z} from "zod";
export type User = z.infer<typeof UserUncheckedCreateInputObjectSchema>;
export const insertUserSchema = (
UserUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).pick({
username: true,
password: true,
});
export const loginSchema = (insertUserSchema as unknown as z.ZodObject<any>).extend({
rememberMe: z.boolean().optional(),
});
export const registerSchema = (insertUserSchema as unknown as z.ZodObject<any>)
.extend({
confirmPassword: z.string().min(6, {
message: "Password must be at least 6 characters long",
}),
agreeTerms: z.literal(true, {
errorMap: () => ({
message: "You must agree to the terms and conditions",
}),
}),
})
.refine((data: any) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
export type InsertUser = z.infer<typeof insertUserSchema>;
export type LoginFormValues = z.infer<typeof loginSchema>;
export type RegisterFormValues = z.infer<typeof registerSchema>;