mirror of
https://github.com/temetro/temetro.git
synced 2026-08-24 08:46:27 +00:00
frontend: messages search, care-team employee dialog, PCP picker + transfer, activity detail, analytics grid
- Messages: search the inbox and the compose member picker. - Care Team: clickable member rows open an employee dialog showing role + permissions, with change-role (updateMemberRole) and remove. - Patients: Primary Care is now a provider dropdown (defaults to self for a doctor); add a Transfer action + dialog wired to the transfer API. - Activity: entries are clickable, opening a detail dialog. - Analytics: Section takes a columns prop so each row fills evenly (no orphan card in Appointments). - Add lib/staff.ts (listProviders), transferPatient client, rolePermissionSummary helper, and i18n keys for all new strings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -187,6 +187,20 @@
|
||||
"notFound": "Patient not found",
|
||||
"loading": "Loading patient…",
|
||||
"noPatientForFile": "No patient found for file #{{number}}."
|
||||
},
|
||||
"transfer": {
|
||||
"action": "Transfer",
|
||||
"title": "Transfer patient",
|
||||
"description": "Reassign {{name}} to another clinician. They become this patient's primary provider.",
|
||||
"providerLabel": "New primary provider",
|
||||
"choose": "Choose a provider…",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Transfer patient",
|
||||
"transferring": "Transferring…",
|
||||
"successTitle": "Patient transferred",
|
||||
"successBody": "{{name}} is now with {{provider}}.",
|
||||
"errorTitle": "Couldn't transfer patient",
|
||||
"error": "Please try again."
|
||||
}
|
||||
},
|
||||
"appointments": {
|
||||
@@ -366,6 +380,8 @@
|
||||
"inbox": "Inbox",
|
||||
"unread": "Unread · {{count}}",
|
||||
"newMessage": "New message",
|
||||
"searchPlaceholder": "Search conversations",
|
||||
"noMatches": "No conversations match your search.",
|
||||
"noUnread": "No unread messages.",
|
||||
"noConversations": "No conversations yet.",
|
||||
"you": "You: ",
|
||||
@@ -378,6 +394,8 @@
|
||||
"compose": {
|
||||
"title": "New message",
|
||||
"description": "Start a conversation with a member of your clinic.",
|
||||
"searchPlaceholder": "Search people",
|
||||
"noMatches": "No people match your search.",
|
||||
"noMembers": "No other clinic members yet. Invite colleagues from Settings → Care team."
|
||||
},
|
||||
"startFailedTitle": "Couldn't start conversation",
|
||||
@@ -430,7 +448,23 @@
|
||||
"changesToday": "Changes today",
|
||||
"thisWeek": "This week",
|
||||
"totalRecorded": "Total recorded",
|
||||
"empty": "No activity yet. Changes to patients, notes, appointments, prescriptions and tasks will appear here."
|
||||
"empty": "No activity yet. Changes to patients, notes, appointments, prescriptions and tasks will appear here.",
|
||||
"detail": {
|
||||
"title": "Activity detail",
|
||||
"person": "Performed by",
|
||||
"record": "Record type",
|
||||
"patient": "Patient",
|
||||
"reference": "Reference",
|
||||
"time": "Time",
|
||||
"close": "Close",
|
||||
"entityTypes": {
|
||||
"patient": "Patient",
|
||||
"note": "Note",
|
||||
"appointment": "Appointment",
|
||||
"prescription": "Prescription",
|
||||
"task": "Task"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notes": {
|
||||
"title": "Notes",
|
||||
@@ -603,6 +637,7 @@
|
||||
"status": "Status",
|
||||
"primaryCare": "Primary care",
|
||||
"primaryCarePlaceholder": "e.g. Dr. Lena Ortiz",
|
||||
"primaryCareUnassigned": "Unassigned",
|
||||
"currentVitals": "Current vitals",
|
||||
"bp": "Blood pressure",
|
||||
"hr": "Heart rate",
|
||||
@@ -725,6 +760,32 @@
|
||||
"you": "(you)",
|
||||
"addMember": "Add team member",
|
||||
"removeMember": "Remove member",
|
||||
"employee": {
|
||||
"title": "Team member",
|
||||
"description": "View this member's access, change their role, or remove them.",
|
||||
"permissions": "Permissions",
|
||||
"noAccess": "No access",
|
||||
"changeRole": "Change role",
|
||||
"save": "Save",
|
||||
"saving": "Saving…",
|
||||
"remove": "Remove employee",
|
||||
"close": "Close",
|
||||
"roleUpdatedTitle": "Role updated",
|
||||
"roleUpdatedBody": "{{name}} is now {{role}}.",
|
||||
"roleFailedTitle": "Couldn't update role",
|
||||
"roleFailedBody": "Please try again.",
|
||||
"resources": {
|
||||
"patient": "Patients",
|
||||
"appointment": "Appointments",
|
||||
"prescription": "Prescriptions",
|
||||
"task": "Tasks"
|
||||
},
|
||||
"actions": {
|
||||
"read": "View",
|
||||
"write": "Edit",
|
||||
"delete": "Delete"
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"title": "Remove team member?",
|
||||
"description": "{{name}} will lose access to this clinic. This can't be undone.",
|
||||
|
||||
@@ -62,7 +62,8 @@ export type Patient = {
|
||||
name: string;
|
||||
age: number;
|
||||
sex: "M" | "F";
|
||||
pcp: string; // primary care provider
|
||||
pcp: string; // primary care provider (display name)
|
||||
primaryProviderId?: string | null; // user id of the responsible clinician
|
||||
status: "active" | "inpatient" | "discharged";
|
||||
initials: string; // for AvatarFallback
|
||||
allergies: Allergy[];
|
||||
@@ -112,6 +113,20 @@ export async function updatePatient(patient: Patient): Promise<Patient> {
|
||||
);
|
||||
}
|
||||
|
||||
// Reassign a patient to another clinician (sets their primary provider + PCP).
|
||||
export async function transferPatient(
|
||||
fileNumber: string,
|
||||
providerId: string,
|
||||
): Promise<Patient> {
|
||||
return apiFetch<Patient>(
|
||||
`/api/patients/${encodeURIComponent(fileNumber)}/transfer`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ providerId }),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Suggest a unique-ish 5-digit file number for new charts. The server is the
|
||||
// source of truth and rejects collisions with a 409.
|
||||
export function generateFileNumber(): string {
|
||||
|
||||
@@ -45,6 +45,42 @@ export function useActiveRole(): string | null {
|
||||
return role;
|
||||
}
|
||||
|
||||
// The clinical resources + actions we surface in the Care Team permissions
|
||||
// summary. Mirrors the statements in lib/access.ts.
|
||||
export const CLINICAL_RESOURCES = [
|
||||
"patient",
|
||||
"appointment",
|
||||
"prescription",
|
||||
"task",
|
||||
] as const;
|
||||
const RESOURCE_ACTIONS = ["read", "write", "delete"] as const;
|
||||
|
||||
type PermissionArg = Parameters<
|
||||
typeof authClient.organization.checkRolePermission
|
||||
>[0]["permissions"];
|
||||
|
||||
// For a given role, the allowed actions on each clinical resource — computed
|
||||
// from Better Auth so it stays in lock-step with lib/access.ts. Used by the
|
||||
// Care Team employee dialog to show what a role can do.
|
||||
export function rolePermissionSummary(
|
||||
role: string | null | undefined,
|
||||
): { resource: (typeof CLINICAL_RESOURCES)[number]; actions: string[] }[] {
|
||||
if (!role) return [];
|
||||
return CLINICAL_RESOURCES.map((resource) => {
|
||||
const actions = RESOURCE_ACTIONS.filter((action) => {
|
||||
try {
|
||||
return authClient.organization.checkRolePermission({
|
||||
role: role as RoleKey,
|
||||
permissions: { [resource]: [action] } as PermissionArg,
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return { resource, actions };
|
||||
});
|
||||
}
|
||||
|
||||
// Whether a role may see clinical records (AI lookup, prescriptions, notes,
|
||||
// analysis). Driven by Better Auth permissions so it stays in lock-step with
|
||||
// lib/access.ts: the `reception` role has no `prescription` statement, so this
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { apiFetch } from "@/lib/api-client";
|
||||
|
||||
// A clinician who can be assigned as a patient's primary provider. Returned by
|
||||
// the backend's GET /api/staff/providers (clinical roles only — excludes
|
||||
// reception/viewer). Readable by any clinic member.
|
||||
export type Provider = {
|
||||
userId: string;
|
||||
name: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
export function listProviders(): Promise<Provider[]> {
|
||||
return apiFetch<Provider[]>("/api/staff/providers");
|
||||
}
|
||||
Reference in New Issue
Block a user