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:
Khalid Abdi
2026-06-09 19:51:38 +03:00
parent 6a0fab97ae
commit 3eb5687f4d
13 changed files with 814 additions and 79 deletions
+62 -1
View File
@@ -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.",
+16 -1
View File
@@ -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 {
+36
View File
@@ -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
+14
View File
@@ -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");
}