fix: add patient form validation and auth proxy

- Fix dateOfBirth default from empty string to null (caused Invalid date error)
- Add noValidate to form to prevent browser native email validation blocking submit
- Reset form when switching from edit to add mode
- Export API_BASE_URL from queryClient; switch patient table to raw fetch (prevents token wipe on 401)
- Add Authorization header forwarding in Vite proxy (was stripped by nginx Connection:upgrade)
- Make only firstName, lastName, dateOfBirth, phone required; gender optional
- Add +1 prefix to phone number input (stores as 1XXXXXXXXXX)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-02 13:15:00 -04:00
parent e26ebf7fd5
commit 3d409d4a84
5 changed files with 68 additions and 20 deletions

View File

@@ -67,13 +67,13 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
...sanitizedPatient, ...sanitizedPatient,
dateOfBirth: patient.dateOfBirth dateOfBirth: patient.dateOfBirth
? formatLocalDate(new Date(patient.dateOfBirth)) ? formatLocalDate(new Date(patient.dateOfBirth))
: "", : null,
}; };
} }
return { return {
firstName: extractedInfo?.firstName || "", firstName: extractedInfo?.firstName || "",
lastName: extractedInfo?.lastName || "", lastName: extractedInfo?.lastName || "",
dateOfBirth: extractedInfo?.dateOfBirth || "", dateOfBirth: extractedInfo?.dateOfBirth || null,
gender: "", gender: "",
phone: "", phone: "",
email: "", email: "",
@@ -119,9 +119,11 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
...sanitizedPatient, ...sanitizedPatient,
dateOfBirth: patient.dateOfBirth dateOfBirth: patient.dateOfBirth
? formatLocalDate(new Date(patient.dateOfBirth)) ? formatLocalDate(new Date(patient.dateOfBirth))
: "", : null,
}; };
form.reset(resetValues); form.reset(resetValues);
} else {
form.reset(computedDefaultValues);
} }
}, [patient, computedDefaultValues, form]); }, [patient, computedDefaultValues, form]);
@@ -134,6 +136,7 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
<form <form
id="patient-form" id="patient-form"
key={patient?.id || "new"} key={patient?.id || "new"}
noValidate
onSubmit={form.handleSubmit((data) => { onSubmit={form.handleSubmit((data) => {
handleSubmit2(data); handleSubmit2(data);
})} })}
@@ -185,7 +188,7 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
name="gender" name="gender"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Gender *</FormLabel> <FormLabel>Gender</FormLabel>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value as string} defaultValue={field.value as string}
@@ -217,15 +220,36 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
<FormField <FormField
control={form.control} control={form.control}
name="phone" name="phone"
render={({ field }) => ( render={({ field }) => {
<FormItem> const display = (field.value as string)?.startsWith("1")
<FormLabel>Phone Number *</FormLabel> ? (field.value as string).slice(1)
<FormControl> : (field.value as string) || "";
<Input {...field} /> return (
</FormControl> <FormItem>
<FormMessage /> <FormLabel>Phone Number *</FormLabel>
</FormItem> <FormControl>
)} <div className="flex">
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-input bg-muted text-muted-foreground text-sm">
+1
</span>
<Input
className="rounded-l-none"
placeholder="6175551234"
value={display}
onChange={(e) => {
const digits = e.target.value.replace(/\D/g, "").slice(0, 10);
field.onChange(digits ? "1" + digits : "");
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/> />
<FormField <FormField
@@ -310,7 +334,7 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
return ( return (
<FormItem> <FormItem>
<FormLabel>Status *</FormLabel> <FormLabel>Status</FormLabel>
<Select <Select
value={(field.value as PatientStatus) ?? "UNKNOWN"} value={(field.value as PatientStatus) ?? "UNKNOWN"}
onValueChange={(v) => onValueChange={(v) =>

View File

@@ -18,7 +18,7 @@ import {
PaginationNext, PaginationNext,
PaginationPrevious, PaginationPrevious,
} from "@/components/ui/pagination"; } from "@/components/ui/pagination";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient, API_BASE_URL } from "@/lib/queryClient";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import LoadingScreen from "@/components/ui/LoadingScreen"; import LoadingScreen from "@/components/ui/LoadingScreen";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
@@ -749,11 +749,21 @@ export function PatientTable({
url = `/api/patients/recent?limit=${patientsPerPage}&offset=${offset}`; url = `/api/patients/recent?limit=${patientsPerPage}&offset=${offset}`;
} }
const res = await apiRequest("GET", url); const token = localStorage.getItem("token");
const res = await fetch(`${API_BASE_URL}${url}`, {
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
credentials: "include",
});
if (res.status === 401 || res.status === 403) {
return { patients: [], totalCount: 0 };
}
if (!res.ok) { if (!res.ok) {
const errorData = await res.json(); const errorData = await res.json().catch(() => ({}));
throw new Error(errorData.message || "Search failed"); throw new Error(errorData.message || "Failed to load patients");
} }
return res.json(); return res.json();

View File

@@ -1,6 +1,6 @@
import { QueryClient, QueryFunction } from "@tanstack/react-query"; import { QueryClient, QueryFunction } from "@tanstack/react-query";
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL_BACKEND ?? ""; export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL_BACKEND ?? "";
async function throwIfResNotOk(res: Response) { async function throwIfResNotOk(res: Response) {
if (!res.ok) { if (!res.ok) {

View File

@@ -18,6 +18,12 @@ export default defineConfig(({ mode }) => {
"/api": { "/api": {
target: env.VITE_API_BASE_URL_BACKEND || "http://localhost:5000", target: env.VITE_API_BASE_URL_BACKEND || "http://localhost:5000",
changeOrigin: true, changeOrigin: true,
configure: (proxy) => {
proxy.on("proxyReq", (proxyReq, req) => {
const auth = (req as any).headers["authorization"];
if (auth) proxyReq.setHeader("Authorization", auth);
});
},
}, },
"/socket.io": { "/socket.io": {
target: env.VITE_API_BASE_URL_BACKEND || "http://localhost:5000", target: env.VITE_API_BASE_URL_BACKEND || "http://localhost:5000",

View File

@@ -51,7 +51,15 @@ export const insertPatientSchema = (
createdAt: true, createdAt: true,
}) })
.extend({ .extend({
insuranceId: insuranceIdSchema, // enforce numeric insuranceId firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
dateOfBirth: z.preprocess(
(val) => (val === null || val === undefined || val === "" ? undefined : val),
z.coerce.date({ required_error: "Date of birth is required" })
),
gender: z.string().optional().nullable(),
phone: z.string().min(1, "Phone number is required"),
insuranceId: insuranceIdSchema,
}); });
export type InsertPatient = z.infer<typeof insertPatientSchema>; export type InsertPatient = z.infer<typeof insertPatientSchema>;