Wire frontend to the backend: auth, organizations, real patient API

Connects the UI-only app to the new backend over Better Auth + a small
patient API client, replacing the in-memory fixture and placeholder identity.

- Better Auth React client (lib/auth-client.ts) + shared access-control
  roles; API client (lib/api-client.ts) sending credentials cross-origin.
- Designed (auth) route group: login, signup, verify-email, forgot/reset
  password, clinic onboarding, and accept-invite.
- proxy.ts (this Next's renamed middleware) optimistic redirect + an
  authoritative client AppAuthGuard requiring a session and active clinic.
- Real identity in nav-user (useSession + sign out); the unused team
  switcher repurposed as an organization (clinic) switcher; Care-team
  settings tab manages members and invitations.
- lib/patients.ts now calls the org-scoped API (types unchanged); patients
  table and create/edit dialog updated for async create/update.
- next.config: standalone output + Dockerfile for containerized runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-02 21:28:02 +03:00
parent dec150c77d
commit 1ecbd16404
31 changed files with 1853 additions and 286 deletions
+8 -1
View File
@@ -70,7 +70,14 @@ export function ChatPanel() {
},
]);
const patient = await getPatient(fileNumber);
let patient: Patient | null = null;
try {
patient = await getPatient(fileNumber);
} catch {
// Network / auth errors fall through as "not found"; a 401 will have
// already redirected to /login via the API client.
patient = null;
}
setMessages((prev) =>
prev.map((message) =>
message.id === resultId &&
@@ -16,11 +16,12 @@ import {
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import {
addPatient,
type AllergySeverity,
createPatient,
generateFileNumber,
type LabFlag,
type Patient,
updatePatient,
} from "@/lib/patients";
type PatientFormDialogProps = {
@@ -127,6 +128,9 @@ export function PatientFormDialog({
}: PatientFormDialogProps) {
const isEdit = mode === "edit";
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [fileNumber, setFileNumber] = useState(() =>
isEdit && patient ? patient.fileNumber : generateFileNumber()
);
@@ -163,9 +167,9 @@ export function PatientFormDialog({
})) ?? []
);
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!name.trim()) {
if (!name.trim() || submitting) {
return;
}
@@ -207,13 +211,25 @@ export function PatientFormDialog({
})),
};
addPatient(built);
if (isEdit) {
onSaved?.(built);
} else {
onCreated?.(fileNumber);
setSubmitting(true);
setError(null);
try {
const saved = isEdit
? await updatePatient(built)
: await createPatient(built);
if (isEdit) {
onSaved?.(saved);
} else {
onCreated?.(saved.fileNumber);
}
onOpenChange(false);
} catch (err) {
setError(
err instanceof Error ? err.message : "Could not save the patient.",
);
} finally {
setSubmitting(false);
}
onOpenChange(false);
};
return (
@@ -497,12 +513,15 @@ export function PatientFormDialog({
/>
</div>
<DialogFooter>
<DialogFooter className="flex-col items-stretch gap-2 sm:flex-row sm:items-center">
{error && (
<p className="text-sm text-destructive sm:mr-auto">{error}</p>
)}
<DialogClose render={<Button type="button" variant="outline" />}>
Cancel
</DialogClose>
<Button disabled={!name.trim()} type="submit">
{isEdit ? "Save changes" : "Save patient"}
<Button disabled={!name.trim() || submitting} type="submit">
{submitting ? "Saving…" : isEdit ? "Save changes" : "Save patient"}
</Button>
</DialogFooter>
</form>