Files
temetro/frontend/lib/portal.ts
T
Khalid Abdi 90e6ec4cc0 feat: email provider, admin password reset, portal new-patient, chat pill
Chat: history pill now shows a History icon + a Start-new-chat (SquarePen)
button; removed the duplicate chat-history list from the sidebar.

Email: deployment-wide email provider config (Resend/Postmark/SendGrid/SMTP) in
Settings → Developers, with encrypted API key and a Send-test action. sendEmail
dispatches via the chosen provider (REST via fetch; SMTP via nodemailer).

Forgot password with no provider: alert the clinic admin(s) via a "System"
message card in Messages + a bell notification (seeded system user + per-clinic
System conversation); clicking deep-links to /settings?tab=careTeam&member=<id>.
Admins can set a member's password directly from the employee dialog
(PATCH /api/staff/:id/password via Better Auth's internal context — no admin
plugin needed).

Patient Portal: "New patient" booking path registers a demographics-only patient
then books; bookings reject double-booked slots (409).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:52:55 +03:00

104 lines
2.4 KiB
TypeScript

// Client for the public Patient Portal kiosk API (backend src/routes/portal.ts).
// Unlike lib/api-client, these calls are unauthenticated (no session cookie) and
// must NOT bounce to /login on error — the kiosk has no login.
import { API_BASE_URL } from "@/lib/api-client";
export type PortalClinic = { name: string };
export type PortalBooking = {
fileNumber: string;
name: string;
date: string; // YYYY-MM-DD
time: string; // HH:mm
type?: string;
};
export type PortalBookingResult = {
date: string;
time: string;
type: string;
provider: string;
};
export type PortalResults = {
name: string;
upcoming: {
date: string;
time: string;
type: string;
provider: string;
status: string;
}[];
hasResults: boolean;
resultCount: number;
};
export class PortalError extends Error {
constructor(
public status: number,
message: string,
) {
super(message);
this.name = "PortalError";
}
}
async function portalFetch<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE_URL}/api/portal${path}`, {
...init,
headers: { "Content-Type": "application/json", ...init?.headers },
});
const body = (await res.json().catch(() => null)) as
| (T & { error?: string })
| null;
if (!res.ok) {
throw new PortalError(
res.status,
body?.error ?? `Request failed (${res.status}).`,
);
}
return body as T;
}
export function getPortalClinic(clinic: string): Promise<PortalClinic> {
return portalFetch<PortalClinic>(`/${encodeURIComponent(clinic)}`);
}
export type PortalNewPatient = {
name: string;
sex?: string;
age?: number;
};
export function createPortalPatient(
clinic: string,
patient: PortalNewPatient,
): Promise<{ fileNumber: string; name: string }> {
return portalFetch(`/${encodeURIComponent(clinic)}/patients`, {
method: "POST",
body: JSON.stringify(patient),
});
}
export function bookPortalAppointment(
clinic: string,
booking: PortalBooking,
): Promise<PortalBookingResult> {
return portalFetch<PortalBookingResult>(
`/${encodeURIComponent(clinic)}/appointments`,
{ method: "POST", body: JSON.stringify(booking) },
);
}
export function lookupPortalResults(
clinic: string,
fileNumber: string,
name: string,
): Promise<PortalResults> {
const qs = new URLSearchParams({ fileNumber, name }).toString();
return portalFetch<PortalResults>(
`/${encodeURIComponent(clinic)}/results?${qs}`,
);
}