mirror of
https://github.com/temetro/temetro.git
synced 2026-08-26 02:17:22 +00:00
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>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { apiFetch } from "@/lib/api-client";
|
||||
|
||||
// Deployment-wide email provider config (Settings → Developers). The API key is
|
||||
// never returned — `hasCredentials` only signals whether one is stored.
|
||||
export type EmailProvider = "none" | "smtp" | "resend" | "postmark" | "sendgrid";
|
||||
|
||||
export type EmailConfig = {
|
||||
provider: EmailProvider;
|
||||
fromAddress: string;
|
||||
hasCredentials: boolean;
|
||||
};
|
||||
|
||||
export function getEmailConfig(): Promise<EmailConfig> {
|
||||
return apiFetch<EmailConfig>("/api/settings/email");
|
||||
}
|
||||
|
||||
export function saveEmailConfig(input: {
|
||||
provider: EmailProvider;
|
||||
fromAddress: string;
|
||||
// undefined = leave existing key; "" = clear; string = set/replace.
|
||||
credentials?: string;
|
||||
}): Promise<EmailConfig> {
|
||||
return apiFetch<EmailConfig>("/api/settings/email", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function testEmailConfig(): Promise<{ ok: boolean; to: string }> {
|
||||
return apiFetch("/api/settings/email/test", { method: "POST" });
|
||||
}
|
||||
@@ -831,7 +831,12 @@
|
||||
"apptPatient": "Patient",
|
||||
"apptStatus": "Status"
|
||||
},
|
||||
"startCall": "Start a call with {{name}}"
|
||||
"startCall": "Start a call with {{name}}",
|
||||
"system": {
|
||||
"label": "System message",
|
||||
"passwordResetTitle": "Password reset requested",
|
||||
"passwordResetBody": "{{name}} forgot their password. Tap to open their settings and set a new one."
|
||||
}
|
||||
},
|
||||
"analysis": {
|
||||
"title": "Overview",
|
||||
@@ -1531,7 +1536,33 @@
|
||||
"generateToken": "Generate token",
|
||||
"resourcesTitle": "Resources",
|
||||
"resourcesDescription": "Where to learn more",
|
||||
"resourcesBody": "The API reference — covering patients, appointments, prescriptions, tasks, messaging, activity and analytics — lives in the project documentation under docs/api. temetro is open source, so the route handlers in backend/src/routes are the authoritative spec."
|
||||
"resourcesBody": "The API reference — covering patients, appointments, prescriptions, tasks, messaging, activity and analytics — lives in the project documentation under docs/api. temetro is open source, so the route handlers in backend/src/routes are the authoritative spec.",
|
||||
"email": {
|
||||
"title": "Email provider",
|
||||
"description": "How temetro sends verification, password-reset and invitation emails. This is a deployment-wide setting.",
|
||||
"provider": "Provider",
|
||||
"providers": {
|
||||
"none": "Not configured",
|
||||
"smtp": "SMTP",
|
||||
"resend": "Resend",
|
||||
"postmark": "Postmark",
|
||||
"sendgrid": "SendGrid"
|
||||
},
|
||||
"from": "From address",
|
||||
"apiKey": "API key",
|
||||
"apiKeySet": "•••••••• (saved — leave blank to keep)",
|
||||
"apiKeyPlaceholder": "Paste your provider API key",
|
||||
"smtpHint": "SMTP uses the SMTP_HOST/PORT/USER/PASS environment variables on the server.",
|
||||
"save": "Save",
|
||||
"saving": "Saving…",
|
||||
"savedTitle": "Email settings saved",
|
||||
"savedFailed": "Couldn't save email settings.",
|
||||
"test": "Send test",
|
||||
"testing": "Sending…",
|
||||
"testSentTitle": "Test email sent",
|
||||
"testSentBody": "Sent a test email to {{to}}.",
|
||||
"testFailed": "Couldn't send the test email."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"sectionTitle": "Clinician profile",
|
||||
@@ -1657,7 +1688,20 @@
|
||||
"read": "View",
|
||||
"write": "Edit",
|
||||
"delete": "Delete"
|
||||
}
|
||||
},
|
||||
"resetPassword": "Reset password",
|
||||
"resetPasswordHint": "Set a new password for this employee (e.g. they forgot theirs and no email provider is configured).",
|
||||
"newPassword": "New password (min 12 chars)",
|
||||
"confirmPassword": "Confirm password",
|
||||
"setPassword": "Set password",
|
||||
"pwTooShortTitle": "Password too short",
|
||||
"pwTooShortBody": "Use at least 12 characters.",
|
||||
"pwMismatchTitle": "Passwords don't match",
|
||||
"pwMismatchBody": "Re-enter the same password in both fields.",
|
||||
"pwUpdatedTitle": "Password updated",
|
||||
"pwUpdatedBody": "{{name}} can now sign in with the new password.",
|
||||
"pwFailedTitle": "Couldn't set password",
|
||||
"pwFailedBody": "Please try again."
|
||||
},
|
||||
"remove": {
|
||||
"title": "Remove team member?",
|
||||
@@ -1810,14 +1854,24 @@
|
||||
"date": "Date",
|
||||
"time": "Time",
|
||||
"reason": "Reason for visit (optional)",
|
||||
"reasonPlaceholder": "e.g. Follow-up, check-up"
|
||||
"reasonPlaceholder": "e.g. Follow-up, check-up",
|
||||
"sex": "Sex",
|
||||
"sexMale": "Male",
|
||||
"sexFemale": "Female",
|
||||
"age": "Age"
|
||||
},
|
||||
"book": {
|
||||
"title": "Book an appointment",
|
||||
"submit": "Request appointment",
|
||||
"successTitle": "You're booked",
|
||||
"successBody": "Your appointment is set for {{date}} at {{time}}. Please check in at the front desk.",
|
||||
"errorGeneric": "Couldn't book the appointment. Please try again or ask the front desk."
|
||||
"errorGeneric": "Couldn't book the appointment. Please try again or ask the front desk.",
|
||||
"mode": {
|
||||
"returning": "Returning patient",
|
||||
"new": "New patient"
|
||||
},
|
||||
"newFileNote": "Your new file number is {{file}} — keep it for next time.",
|
||||
"slotTaken": "That time is already taken. Please choose another."
|
||||
},
|
||||
"results": {
|
||||
"title": "View my results",
|
||||
|
||||
@@ -26,7 +26,13 @@ export type MessageAttachment =
|
||||
mimeType: string;
|
||||
size: number;
|
||||
}
|
||||
| { kind: "appointment"; appointment: AppointmentSnapshot };
|
||||
| { kind: "appointment"; appointment: AppointmentSnapshot }
|
||||
| {
|
||||
kind: "passwordReset";
|
||||
userId: string;
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
};
|
||||
|
||||
export type ConversationMessage = {
|
||||
id: string;
|
||||
|
||||
@@ -65,6 +65,22 @@ 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,
|
||||
|
||||
@@ -15,6 +15,18 @@ export function listProviders(): Promise<Provider[]> {
|
||||
return apiFetch<Provider[]>("/api/staff/providers");
|
||||
}
|
||||
|
||||
// Set a member's password directly (owner/admin only) — used when an employee
|
||||
// forgot it and no email provider is configured.
|
||||
export function setStaffPassword(
|
||||
userId: string,
|
||||
newPassword: string,
|
||||
): Promise<{ ok: boolean }> {
|
||||
return apiFetch(`/api/staff/${encodeURIComponent(userId)}/password`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ newPassword }),
|
||||
});
|
||||
}
|
||||
|
||||
// Update a member's clinical specialty (owner/admin only). Pass null to clear.
|
||||
export function updateStaffSpecialty(
|
||||
userId: string,
|
||||
|
||||
Reference in New Issue
Block a user