Skip to content

Commit 14350fe

Browse files
authored
Merge branch 'develop' into issues/15661/template-page
2 parents cf66b27 + 37087ab commit 14350fe

54 files changed

Lines changed: 1562 additions & 1114 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.example.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ REACT_SENTRY_ENVIRONMENT=
4747
# Flag to allow some fields in patient registration to be non-mandatory
4848
REACT_ENABLE_MINIMAL_PATIENT_REGISTRATION=true
4949

50+
# Flag to bypass patient edit access permissions
51+
REACT_PATIENT_GLOBAL_EDIT_ACCESS_ENABLED=false
52+
5053
# Minimum number of geo-organization levels required during patient registration
5154
# If not set, disables the requirement
5255
REACT_PATIENT_REG_MIN_GEO_ORG_LEVELS_REQUIRED=

CLAUDE.md

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,122 @@
22

33
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
44

5+
## What is CARE?
6+
7+
CARE is a Digital Public Good building an open source EMR + Hospital Management system. This is the React frontend (React 19 + TypeScript + Vite).
8+
59
## Build/Lint/Test Commands
6-
- `npm run dev`: Start development server
7-
- `npm run build`: Build for production
8-
- `npm run lint`: Run ESLint
9-
- `npm run lint-fix`: Run ESLint with auto-fix
10-
- `npm run format`: Format code with Prettier
11-
- `npm run playwright:test`: Run Playwright tests in headless mode
12-
- `npm run playwright:test:ui`: Run Playwright tests in interactive UI mode
10+
11+
- `npm run dev` — Start dev server at http://localhost:4000
12+
- `npm run build` — Production build (takes 2+ minutes, set timeout to 180s+)
13+
- `npm run lint` — Run ESLint (takes 85s+, set timeout to 120s+)
14+
- `npm run lint-fix` — ESLint with auto-fix
15+
- `npm run format` — Prettier formatting
16+
- `npm run playwright:test` — Run all Playwright E2E tests headlessly
17+
- `npm run playwright:test -- tests/auth/login.spec.ts` — Run a single test file
18+
- `npm run playwright:test -- -g "test name"` — Run tests matching a pattern
19+
- `npm run playwright:test:ui` — Interactive Playwright UI mode
20+
21+
Playwright requires a local backend running (`REACT_CARE_API_URL=http://127.0.0.1:9000` in `.env.local`) and `npm run playwright:install` for browsers.
1322

1423
## Code Style Guidelines
15-
- **TypeScript**: Strict mode, ES2022 target, path aliases (`@/*` for src)
24+
25+
- **TypeScript**: Strict mode, ES2022 target, path aliases (`@/*``src/*`, `@careConfig``care.config.ts`)
1626
- **Formatting**: Double quotes, 2-space indent, semicolons required
17-
- **Imports**: Order by 3rd-party → library → CAREUI → UI → components → hooks → utils → relative
18-
- **Types**: Use `interface` for objects, avoid explicit `any`, proper nullability
19-
- **Naming**: PascalCase for components/classes, camelCase for variables/functions
20-
- **Components**: Organized by feature, maintain separation of concerns
21-
- **Error Handling**: Use dedicated error handlers, TypeScript strict null checks
27+
- **Imports**: Order by 3rd-party → library → CAREUI → UI → components → hooks → utils → relative. Prettier plugin auto-sorts on format.
28+
- **Types**: Use `interface` for objects, avoid `any`, prefer maps over enums
29+
- **Naming**: PascalCase for component files (`AuthWizard.tsx`), camelCase for hooks/utils (`useAuth.ts`), kebab-case for directories
30+
- **Components**: Functional components only, named exports preferred, one component per file
31+
- **i18n**: All user-facing strings must use i18next. English translations go in `public/locale/en.json`. Non-English managed via Crowdin — do not edit directly.
32+
33+
## Architecture
34+
35+
### Routing (Raviger)
36+
37+
Routes defined in `src/Routers/routes/` (e.g., `FacilityRoutes.tsx`, `PatientRoutes.tsx`). Combined in `src/Routers/AppRouter.tsx`. Three routers: `PublicRouter`, `PatientRouter`, `AppRouter` — selected by auth state. Plugin routes injected via `usePluginRoutes()`.
38+
39+
```typescript
40+
const FacilityRoutes: AppRoutes = {
41+
"/facility/:facilityId/overview": ({ facilityId }) => <FacilityOverview facilityId={facilityId} />,
42+
};
43+
```
44+
45+
Use `navigate()` from raviger for programmatic navigation.
46+
47+
### API Layer (TanStack Query + custom wrappers)
48+
49+
API routes defined in `src/types/{domain}/{domain}Api.ts` using typed route objects:
50+
51+
```typescript
52+
export default {
53+
list: {
54+
path: "/api/v1/users/",
55+
method: HttpMethod.GET,
56+
TRes: Type<PaginatedResponse<UserReadMinimal>>(),
57+
},
58+
} as const;
59+
```
60+
61+
Queries use `query()` wrapper from `src/Utils/request/query.ts`:
62+
63+
```typescript
64+
const { data } = useQuery({
65+
queryKey: ["users"],
66+
queryFn: query(userApi.list),
67+
});
68+
// With path/query params:
69+
queryFn: query(userApi.get, { pathParams: { username }, queryParams: { search } })
70+
```
71+
72+
Mutations use `mutate()` wrapper from `src/Utils/request/mutate.ts`:
73+
74+
```typescript
75+
const { mutate } = useMutation({
76+
mutationFn: mutate(userApi.create),
77+
});
78+
```
79+
80+
Also available: `query.debounced()` and `query.paginated()` for specialized use cases.
81+
82+
Errors handled globally — session expiry redirects to `/session-expired`, 400/406 show toast notifications. Use `silent: true` to suppress.
83+
84+
### State Management
85+
86+
- **TanStack Query** — Server state (API data caching, refetching)
87+
- **Jotai atoms** (`src/atoms/`) — Lightweight client state (user, nav, filters)
88+
- **React Context** (`src/context/`) — Permissions (`PermissionContext`), keyboard shortcuts
89+
90+
### UI Components
91+
92+
Built on **shadcn/uishadcn/ui** + **Radix UI primitives** + **Tailwind CSS v4** (shadcn/ui pattern):
93+
- `src/components/ui/` — Base UI primitives (Button, Dialog, Form, Select, etc.). Do not modify these directly.
94+
- `src/CAREUI/` — Custom healthcare icon library, use `lucide-react` unless you are explicitly asked to use CAREUI icons.
95+
- Forms use `react-hook-form` + `zod` validation with the custom `<Form>` component
96+
97+
### Plugin System (Module Federation)
98+
99+
Micro-frontend architecture via `@originjs/vite-plugin-federation`. Plugins configured via `REACT_ENABLED_APPS` env var. Plugin manifests define routes, components, tabs, and devices they provide. Key files: `src/PluginEngine.tsx`, `src/pluginTypes.ts`.
100+
101+
### Auth Flow
102+
103+
JWT tokens in localStorage. `AuthUserProvider` handles login/logout, token refresh (every 5 minutes), 2FA, and cross-tab session sync. Patient login uses separate OTP-based flow via `PatientRouter`.
104+
105+
### Key Directories
106+
107+
- `src/components/` — Feature-organized components (Auth, Facility, Patient, Encounter, Medicine, etc.)
108+
- `src/pages/` — Page components by feature (Admin, Appointments, Facility, Organization, Patient)
109+
- `src/types/` — Domain type definitions with corresponding `*Api.ts` route files
110+
- `src/Utils/request/` — API request infrastructure (query, mutate, error handling)
111+
- `src/hooks/` — Custom React hooks (auth, file management, plugins, etc.)
112+
- `src/Providers/` — Auth, history, patient user providers
113+
- `src/Routers/` — App routing and route definitions
114+
115+
### Configuration
116+
117+
`care.config.ts` centralizes runtime config (API URLs, feature flags, locale settings, plugin config). Environment variables prefixed with `REACT_`.
118+
119+
## Git Workflow
120+
121+
- Branch naming: `issues/{issue#}/{short-name}`
122+
- Default branch: `develop` (staging auto-deploys)
123+
- Pre-commit hooks via husky run Prettier and ESLint on staged files

care.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,11 @@ const careConfig = {
249249
env.REACT_ENABLE_MINIMAL_PATIENT_REGISTRATION,
250250
false,
251251
),
252+
253+
globalPatientEditAccessEnabled: booleanFromString(
254+
env.REACT_PATIENT_GLOBAL_EDIT_ACCESS_ENABLED,
255+
false,
256+
),
252257
},
253258

254259
i18nUrl: env.REACT_CUSTOM_REMOTE_I18N_URL,

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@
116116
"normalize-wheel": "^1.0.1",
117117
"pigeon-maps": "^0.22.1",
118118
"qrcode.react": "^4.1.0",
119-
"raviger": "^5.0.0-2",
119+
"raviger": "^5.3.0",
120120
"react": "19.1.1",
121121
"react-day-picker": "^9.6.3",
122122
"react-dom": "19.1.1",

public/locale/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2455,6 +2455,7 @@
24552455
"failed_to_cancel_invoice": "Failed to cancel invoice",
24562456
"failed_to_cancel_payment": "Failed to cancel payment",
24572457
"failed_to_capture_image": "Failed to capture image",
2458+
"failed_to_check_appointments": "Failed to check appointments",
24582459
"failed_to_create_appointment": "Failed to create an appointment",
24592460
"failed_to_create_category": "Failed to create category",
24602461
"failed_to_create_invoice": "Failed to create invoice",
@@ -3480,6 +3481,7 @@
34803481
"no_allergies_recorded_description": "No Allergies have been recorded",
34813482
"no_alternative_names_added": "No alternative names added",
34823483
"no_appointments": "No appointments found",
3484+
"no_appointments_found_for_today": "No matching appointment found for today. Redirecting to Patient Home...",
34833485
"no_attachments_found": "This communication has no attachments.",
34843486
"no_availabilities_yet": "No availabilities yet",
34853487
"no_available_beds_found": "No available beds found",
@@ -5940,6 +5942,10 @@
59405942
"token_queue": "Token Queue",
59415943
"token_queues": "Token Queues",
59425944
"token_revoked_successfully": "Token revoked successfully",
5945+
"token_status__called": "Call Next",
5946+
"token_status__recall": "Recall",
5947+
"token_status__serving": "Serving",
5948+
"token_status__waiting": "Waiting",
59435949
"token_warning_message": "This token will only be shown once. Make sure to copy and store it securely before closing this dialog.",
59445950
"tokens": "Tokens",
59455951
"tokens_left": "{{ count }} left",

scripts/validate-env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ const envSchema = z
117117
REACT_JWT_TOKEN_REFRESH_INTERVAL: numberAsString.optional(),
118118
REACT_DISABLE_PATIENT_LOGIN: booleanAsStringSchema.optional(),
119119
REACT_ENABLE_MINIMAL_PATIENT_REGISTRATION: booleanAsStringSchema.optional(),
120+
REACT_PATIENT_GLOBAL_EDIT_ACCESS_ENABLED: booleanAsStringSchema.optional(),
120121
REACT_APPOINTMENTS_DEFAULT_DATE_FILTER: numberAsString.optional(),
121122
REACT_PAYMENT_LOCATION_REQUIRED: booleanAsStringSchema.optional(),
122123
REACT_ENCOUNTER_DEFAULT_DATE_FILTER: numberAsString.optional(),

src/CAREUI/misc/PrintPreview.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ function FacilityPrintLayout({
172172
const pageStyle = buildPageStyle(printTemplate);
173173

174174
return (
175-
<>
175+
<div className="flex flex-col min-h-[calc(100vh-80px)] print:min-h-[100vh]">
176176
{pageStyle && <style>{pageStyle}</style>}
177177
{headerImage?.url ? (
178178
<div className="flex justify-between items-start mb-4 pb-2">
@@ -209,9 +209,9 @@ function FacilityPrintLayout({
209209
/>
210210
</div>
211211
)}
212-
{children}
212+
<div className="flex-1">{children}</div>
213213
{footerImage?.url && (
214-
<div className="mt-4 pt-2">
214+
<div className="mt-auto pt-2">
215215
<img
216216
src={footerImage.url}
217217
alt="Footer"
@@ -224,6 +224,6 @@ function FacilityPrintLayout({
224224
/>
225225
</div>
226226
)}
227-
</>
227+
</div>
228228
);
229229
}

src/Providers/HistoryAPIProvider.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,23 @@ export const ResetHistoryContext = createContext(() => {});
77

88
export default function HistoryAPIProvider(props: { children: ReactNode }) {
99
const [history, setHistory] = useState<string[]>([]);
10-
1110
useLocationChange(
12-
(newLocation) => {
13-
const newPath = newLocation.fullPath + newLocation.search;
14-
setHistory((history) => {
15-
if (history.length && newPath === history[0])
16-
// Ignore push if navigate to same path (for some weird unknown reasons?)
17-
return history;
11+
(location) => {
12+
const newPath = location.fullPath + location.search;
13+
const action = location.initiatedBy;
1814

19-
if (history.length > 1 && newPath === history[1])
20-
// Pop current path if navigate back to previous path
15+
setHistory((history) => {
16+
// Pop current path if navigate back to previous path
17+
if (
18+
(history.length > 1 && newPath === history[1]) ||
19+
action === "pop"
20+
) {
2121
return history.slice(1);
22+
}
23+
24+
if (action === "replace") {
25+
return [newPath, ...history.slice(1)];
26+
}
2227

2328
// Otherwise just push the current path
2429
return [newPath, ...history];

src/components/Billing/CompactConditionEditor.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,20 @@ interface CompactConditionEditorProps {
4141
availableMetrics: Metrics[];
4242
onChange: (conditions: ConditionForm[]) => void;
4343
className?: string;
44+
facilityId?: string;
4445
}
4546

4647
// Keep only TagSelector as a separate component since it needs to use a hook
4748
function TagSelector({
4849
value,
4950
onChange,
5051
resource,
52+
facilityId,
5153
}: {
5254
value: TagOperationValue;
5355
onChange: (value: TagOperationValue) => void;
5456
resource: TagResource;
57+
facilityId?: string;
5558
}) {
5659
const { tagIds } = extractTagInformation(value);
5760

@@ -78,6 +81,7 @@ function TagSelector({
7881
resource={resource}
7982
onChange={handleChange}
8083
className="h-9 w-full"
84+
facilityId={facilityId}
8185
/>
8286
</div>
8387
);
@@ -87,10 +91,12 @@ function RenderInput({
8791
metric,
8892
operation,
8993
form,
94+
facilityId,
9095
}: {
9196
metric: string;
9297
operation: ConditionOperation;
9398
form: UseFormReturn<ConditionForm, unknown, ConditionForm>;
99+
facilityId?: string;
94100
}) {
95101
const { t } = useTranslation();
96102
// For patient_gender with equality operation
@@ -310,6 +316,7 @@ function RenderInput({
310316
field.onChange(value);
311317
}}
312318
resource={tagResource}
319+
facilityId={facilityId}
313320
/>
314321
</FormControl>
315322
</FormItem>
@@ -392,6 +399,7 @@ export function CompactConditionEditor({
392399
availableMetrics,
393400
onChange,
394401
className = "",
402+
facilityId,
395403
}: CompactConditionEditorProps) {
396404
const { t } = useTranslation();
397405
const [isAdding, setIsAdding] = useState(false);
@@ -564,7 +572,12 @@ export function CompactConditionEditor({
564572
)}
565573
/>
566574

567-
<RenderInput metric={metric} operation={operation} form={form} />
575+
<RenderInput
576+
metric={metric}
577+
operation={operation}
578+
form={form}
579+
facilityId={facilityId}
580+
/>
568581
</div>
569582
{/* Error Summary */}
570583
{Object.keys(form.formState.errors).length > 0 && (

0 commit comments

Comments
 (0)