diff --git a/frontend/components/settings/employee-detail-dialog.tsx b/frontend/components/settings/employee-detail-dialog.tsx index 8d7f1f4..dc0fa81 100644 --- a/frontend/components/settings/employee-detail-dialog.tsx +++ b/frontend/components/settings/employee-detail-dialog.tsx @@ -1,6 +1,12 @@ "use client"; -import { Trash2 } from "lucide-react"; +import { + CalendarDays, + ListChecks, + Pill, + Trash2, + Users, +} from "lucide-react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -17,11 +23,26 @@ import { DialogPopup, DialogTitle, } from "@/components/ui/dialog"; +import { + Select, + SelectItem, + SelectPopup, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { ROLE_LABELS } from "@/lib/access"; import { authClient } from "@/lib/auth-client"; import { PROVISIONABLE_ROLES, rolePermissionSummary } from "@/lib/roles"; import { notify } from "@/lib/toast"; +// Icon shown next to each permission resource row. +const RESOURCE_ICONS: Record = { + patient: , + appointment: , + prescription: , + task: , +}; + // One row of /api/staff — shared with the Care Team panel. export type StaffMember = { id: string; @@ -32,9 +53,6 @@ export type StaffMember = { username: string | null; }; -const selectClass = - "h-9 w-full rounded-3xl border border-transparent bg-input/50 px-3 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30"; - function roleLabel(role?: string | null): string { if (!role) return ROLE_LABELS.member; return (ROLE_LABELS as Record)[role] ?? role; @@ -128,18 +146,18 @@ export function EmployeeDetailDialog({ -
- - +
+ + {initials(member?.name, member?.email)}
-

+

{member?.name || member?.email || member?.userId}

{secondary && ( -

+

{secondary}

)} @@ -153,23 +171,33 @@ export function EmployeeDetailDialog({ {t("settings.careTeam.employee.permissions")} -
+
{summary.map(({ resource, actions }) => (
- + + + {RESOURCE_ICONS[resource]} + {t(`settings.careTeam.employee.resources.${resource}`)} - - {actions.length === 0 - ? t("settings.careTeam.employee.noAccess") - : actions - .map((a) => - t(`settings.careTeam.employee.actions.${a}`), - ) - .join(" · ")} + + {actions.length === 0 ? ( + + {t("settings.careTeam.employee.noAccess")} + + ) : ( + actions.map((a) => ( + + {t(`settings.careTeam.employee.actions.${a}`)} + + )) + )}
))} @@ -182,18 +210,28 @@ export function EmployeeDetailDialog({ {t("settings.careTeam.employee.changeRole")}
- ({ + value: r, + label: roleLabel(r), + }))} + onValueChange={(value) => setRole(value as string)} value={role} > - {roleOptions.map((r) => ( - - ))} - + + + + + {roleOptions.map((r) => ( + + {roleLabel(r)} + + ))} + + + + + + + +

+ {t("settings.developers.resourcesBody")} +

+
+
+ + ); +} diff --git a/frontend/components/settings/settings-records.tsx b/frontend/components/settings/settings-records.tsx new file mode 100644 index 0000000..50c045d --- /dev/null +++ b/frontend/components/settings/settings-records.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { Database, Download, Import, Smartphone } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + SettingsCard, + SettingsSection, +} from "@/components/settings/settings-parts"; + +// A single titled row inside a records card: icon + title/description on the +// left, a status badge or action on the right. +function RecordRow({ + icon, + title, + description, + right, +}: { + icon: React.ReactNode; + title: string; + description: string; + right: React.ReactNode; +}) { + return ( +
+
+
+ {icon} +
+
+

{title}

+

{description}

+
+
+
{right}
+
+ ); +} + +export function RecordsPanel() { + const { t } = useTranslation(); + const comingSoon = ( + {t("settings.records.comingSoon")} + ); + return ( + <> + + + } + right={ + + {t("settings.records.connected")} + + } + title={t("settings.records.clinicDb")} + /> + } + right={comingSoon} + title={t("settings.records.patientStorage")} + /> + + + + + + } + right={ + + } + title={t("settings.records.export")} + /> + } + right={comingSoon} + title={t("settings.records.import")} + /> + + + + + +

+ {t("settings.records.retentionBody")} +

+
+
+ + ); +} diff --git a/frontend/components/settings/settings-view.tsx b/frontend/components/settings/settings-view.tsx index 71f0892..39252a9 100644 --- a/frontend/components/settings/settings-view.tsx +++ b/frontend/components/settings/settings-view.tsx @@ -4,13 +4,11 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; -import { - SettingsCard, - SettingsSection, -} from "@/components/settings/settings-parts"; import { SigningPanel } from "@/components/settings/settings-billing"; import { CareTeamPanel } from "@/components/settings/settings-care-team"; +import { DevelopersPanel } from "@/components/settings/settings-developers"; import { ProfilePanel } from "@/components/settings/settings-preferences"; +import { RecordsPanel } from "@/components/settings/settings-records"; import { useActiveRole } from "@/lib/roles"; const TABS = [ @@ -23,23 +21,6 @@ const TABS = [ type Tab = (typeof TABS)[number]["id"]; -function PlaceholderPanel({ - title, - description, -}: { - title: string; - description: string; -}) { - const { t } = useTranslation(); - return ( - - -

{t("settings.empty")}

-
-
- ); -} - export function SettingsView() { const { t } = useTranslation(); const role = useActiveRole(); @@ -78,20 +59,10 @@ export function SettingsView() {
{activeTab === "profile" && } - {activeTab === "records" && ( - - )} + {activeTab === "records" && } {activeTab === "signing" && } {activeTab === "careTeam" && } - {activeTab === "developers" && ( - - )} + {activeTab === "developers" && }
); diff --git a/frontend/lib/i18n/locales/en/translation.json b/frontend/lib/i18n/locales/en/translation.json index b917d3a..e40937a 100644 --- a/frontend/lib/i18n/locales/en/translation.json +++ b/frontend/lib/i18n/locales/en/translation.json @@ -697,10 +697,41 @@ "copy": "Copy", "copied": "Copied", "records": { - "description": "How patient records are sourced, stored, and displayed" + "description": "How patient records are sourced, stored, and displayed", + "comingSoon": "Coming soon", + "connected": "Connected", + "sourcesTitle": "Data sources", + "sourcesDescription": "Where the patient records shown in temetro come from", + "clinicDb": "Clinic database", + "clinicDbDesc": "Patient records are stored in your clinic's own database and scoped to this clinic.", + "patientStorage": "Patient-owned storage", + "patientStorageDesc": "Read and write records directly on the patient's device once the companion app ships.", + "importExportTitle": "Import & export", + "importExportDescription": "Move records in and out of temetro", + "export": "Export records", + "exportDesc": "Download every patient record in this clinic as a single archive.", + "import": "Import an existing database", + "importDesc": "Connect an existing patient database and browse it in the same card UI.", + "retentionTitle": "Retention", + "retentionDescription": "How long records are kept", + "retentionBody": "Records are kept for as long as your clinic exists. Deleting a patient permanently removes their chart from the clinic database, and the activity log keeps an audit trail of every change — who made it and when." }, "developers": { - "description": "Access tokens for the temetro API" + "description": "Access tokens for the temetro API", + "comingSoon": "Coming soon", + "apiTitle": "API access", + "apiDescription": "Build on the same REST API the temetro app uses", + "baseUrlLabel": "API base URL", + "baseUrlDescription": "All endpoints live under this origin (e.g. /api/patients)", + "authLabel": "Authentication", + "authDescription": "Requests authenticate with your temetro session cookie. Scoped personal access tokens for server-to-server use are planned.", + "tokensTitle": "Personal access tokens", + "tokensDescription": "Long-lived tokens for scripts and integrations", + "noTokens": "No tokens yet.", + "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." }, "profile": { "sectionTitle": "Clinician profile", @@ -776,6 +807,7 @@ "loadError": "Could not load the care team.", "loading": "Loading care team…", "you": "(you)", + "clickHint": "Tip: click a team member to see their permissions and manage their role.", "addMember": "Add team member", "removeMember": "Remove member", "employee": { @@ -855,7 +887,22 @@ "fingerprintDescription": "Share or publish this fingerprint so patients can trust your changes", "signedRecordsTitle": "Signed records", "signedRecordsDescription": "Changes you've signed that are waiting on the patient's approval", - "noPending": "No pending signatures" + "noPending": "No pending signatures", + "comingSoon": "Coming soon", + "howItWorksTitle": "How signing works", + "howItWorksDescription": "The flow behind patient-approved records", + "steps": { + "sign": "You sign every change", + "signDesc": "Each edit you make to a patient record is signed with your private key before it's written.", + "approve": "The patient approves it", + "approveDesc": "Signed changes stay pending until the patient accepts them in the companion app.", + "verify": "Anyone can verify", + "verifyDesc": "Your public key fingerprint lets patients and other clinics verify a change really came from you." + }, + "backupTitle": "Backup key", + "backupDescription": "Recover your signing identity if you lose this device", + "backupLabel": "Recovery phrase", + "backupDesc": "Export an encrypted backup of your signing key to restore it on a new device." } } }