@eka-care/medical-records-ui 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,390 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, MouseEvent } from 'react';
3
+ import { SDKEnvironment, EditRecordRequest, CreateCaseRequest, UpdateCaseRequest } from '@eka-care/medical-records-ts-sdk';
4
+ import * as zustand from 'zustand';
5
+
6
+ type Environment = SDKEnvironment;
7
+ interface DocumentTypeConfig {
8
+ id: string;
9
+ display_name: string;
10
+ hex?: string;
11
+ bg_hex?: string;
12
+ archive?: boolean;
13
+ }
14
+ interface SdkConfig {
15
+ environment?: Environment;
16
+ baseUrl?: string;
17
+ /** Extra headers (client-id, flavour, b-tid, …). */
18
+ defaultHeaders?: Record<string, string>;
19
+ onError?: (error: unknown) => void;
20
+ onUnauthorized?: () => Promise<string | undefined> | string | undefined;
21
+ accessToken?: string;
22
+ transport?: 'native' | 'bridge';
23
+ oid?: string;
24
+ }
25
+
26
+ interface SdkProviderProps {
27
+ config: SdkConfig;
28
+ bid?: string;
29
+ patientId?: string;
30
+ /**
31
+ * Document-type definitions from the host's onboarding config
32
+ * (`mr_document_type`). Drives the code → display-name labels shown across
33
+ * the feature; the SDK's static map is the fallback when a code is absent.
34
+ */
35
+ documentTypes?: DocumentTypeConfig[];
36
+ children: ReactNode;
37
+ }
38
+ declare function SdkProvider({ config, bid, patientId, documentTypes, children }: SdkProviderProps): react.JSX.Element;
39
+
40
+ type DocumentTypeLabelResolver = (code: string) => string;
41
+
42
+ interface RecordItemProps {
43
+ record: RecordItem;
44
+ selected: boolean;
45
+ selectionMode?: boolean;
46
+ thumbnailUrl?: string | null;
47
+ inContext?: boolean;
48
+ attachDisabled?: boolean;
49
+ selectDisabled?: boolean;
50
+ isUploading?: boolean;
51
+ isUploadFailed?: boolean;
52
+ isAnalysing?: boolean;
53
+ isSmart?: boolean;
54
+ dateDisplay: string;
55
+ onSelect: (id: string) => void;
56
+ onOpen: (id: string) => void;
57
+ onDelete: (id: string) => void;
58
+ onAddToContext: (id: string) => void;
59
+ onCopyNotes: (id: string, anchor?: {
60
+ x: number;
61
+ y: number;
62
+ }) => void;
63
+ }
64
+ interface RecordItem {
65
+ id: string;
66
+ patientId: string;
67
+ title: string;
68
+ typeCode: string;
69
+ type: string;
70
+ thumbnailUrl: string | null;
71
+ createdAtEpoch: number;
72
+ updatedAtEpoch: number;
73
+ tags: string[];
74
+ cases: string[];
75
+ syncState: 'uploading' | 'upload_success' | 'upload_failure' | null;
76
+ fileType: string | null;
77
+ isSmart: boolean;
78
+ isAnalysing: boolean;
79
+ }
80
+ interface UploadQueueItem {
81
+ id: string;
82
+ fileName: string;
83
+ mimeType: string;
84
+ sizeBytes: number;
85
+ progress: number;
86
+ status: 'pending' | 'uploading' | 'completed' | 'upload_failure';
87
+ documentId?: string;
88
+ }
89
+ interface CaseUiItem {
90
+ id: string;
91
+ name: string;
92
+ typeCode: string;
93
+ createdAtEpoch: number;
94
+ updatedAtEpoch: number;
95
+ occurredAtEpoch: number | null;
96
+ }
97
+
98
+ type RecordsStatus = 'idle' | 'loading' | 'stale' | 'fresh' | 'error';
99
+ interface RecordsSlice {
100
+ byId: Record<string, RecordItem>;
101
+ ids: string[];
102
+ status: RecordsStatus;
103
+ error: string | null;
104
+ pendingIds: Set<string>;
105
+ setAll: (records: RecordItem[]) => void;
106
+ upsert: (record: RecordItem) => void;
107
+ remove: (id: string) => void;
108
+ markPending: (id: string) => void;
109
+ unmarkPending: (id: string) => void;
110
+ setStatus: (status: RecordsStatus) => void;
111
+ setError: (error: string | null) => void;
112
+ }
113
+
114
+ interface CasesSlice {
115
+ byId: Record<string, CaseUiItem>;
116
+ ids: string[];
117
+ status: RecordsStatus;
118
+ error: string | null;
119
+ setAll: (cases: CaseUiItem[]) => void;
120
+ upsert: (caseItem: CaseUiItem) => void;
121
+ remove: (id: string) => void;
122
+ setStatus: (status: RecordsStatus) => void;
123
+ setError: (error: string | null) => void;
124
+ }
125
+
126
+ interface UploadSlice {
127
+ queue: UploadQueueItem[];
128
+ add: (item: UploadQueueItem) => void;
129
+ updateProgress: (id: string, progress: number) => void;
130
+ markCompleted: (id: string, documentId: string) => void;
131
+ markFailed: (id: string) => void;
132
+ remove: (id: string) => void;
133
+ }
134
+
135
+ interface SelectionSlice {
136
+ recordIds: Set<string>;
137
+ openDetailId: string | null;
138
+ toggleRecord: (id: string, max?: number) => void;
139
+ clearSelection: () => void;
140
+ openDetail: (id: string | null) => void;
141
+ }
142
+
143
+ type DatePreset = 'anytime' | 'last7' | 'last30' | 'last3m' | 'custom';
144
+ interface PopoverFilters {
145
+ documentDateFrom: number | null;
146
+ documentDateTo: number | null;
147
+ uploadDateFrom: number | null;
148
+ uploadDateTo: number | null;
149
+ documentDatePreset: DatePreset;
150
+ uploadDatePreset: DatePreset;
151
+ tags: string[];
152
+ }
153
+ interface FiltersSlice {
154
+ type: string | null;
155
+ search: string;
156
+ documentDateFrom: number | null;
157
+ documentDateTo: number | null;
158
+ uploadDateFrom: number | null;
159
+ uploadDateTo: number | null;
160
+ documentDatePreset: DatePreset;
161
+ uploadDatePreset: DatePreset;
162
+ tags: string[];
163
+ sortBy: 'date' | 'name';
164
+ sortDir: 'asc' | 'desc';
165
+ setType: (type: string | null) => void;
166
+ setSearch: (q: string) => void;
167
+ applyPopoverFilters: (filters: PopoverFilters) => void;
168
+ setSortBy: (by: FiltersSlice['sortBy']) => void;
169
+ setSortDir: (dir: FiltersSlice['sortDir']) => void;
170
+ clearPopoverFilters: () => void;
171
+ reset: () => void;
172
+ }
173
+
174
+ type SyncStatus = 'online' | 'offline' | 'syncing';
175
+ interface SyncSlice {
176
+ status: SyncStatus;
177
+ pendingSyncCount: number;
178
+ setStatus: (status: SyncStatus) => void;
179
+ incrementPending: () => void;
180
+ decrementPending: () => void;
181
+ }
182
+
183
+ type DetailStatus = 'idle' | 'loading' | 'loaded' | 'error';
184
+ interface RecordDetail {
185
+ status: DetailStatus;
186
+ isSmart: boolean;
187
+ /** Verified smart-report fields as `name: value` lines, ready to copy/paste. */
188
+ smartText: string;
189
+ }
190
+ interface DetailsSlice {
191
+ byId: Record<string, RecordDetail>;
192
+ setDetailLoading: (id: string) => void;
193
+ setDetail: (id: string, detail: {
194
+ isSmart: boolean;
195
+ smartText: string;
196
+ }) => void;
197
+ setDetailError: (id: string) => void;
198
+ }
199
+
200
+ /** Composite state for a single patient's store. */
201
+ interface PatientState {
202
+ bid: string;
203
+ patientId: string;
204
+ records: RecordsSlice;
205
+ cases: CasesSlice;
206
+ details: DetailsSlice;
207
+ uploadQueue: UploadSlice;
208
+ selection: SelectionSlice;
209
+ filters: FiltersSlice;
210
+ sync: SyncSlice;
211
+ /** Disposal hook fired when SdkProvider unmounts or the patient changes. */
212
+ __cleanup: () => void;
213
+ }
214
+
215
+ /**
216
+ * Builds one Zustand store per patient. `subscribeWithSelector` is required so
217
+ * per-slice selectors re-render only on the state they read.
218
+ */
219
+ declare function createPatientStore(bid: string, patientId: string): Omit<zustand.StoreApi<PatientState>, "subscribe"> & {
220
+ subscribe: {
221
+ (listener: (selectedState: PatientState, previousSelectedState: PatientState) => void): () => void;
222
+ <U>(selector: (state: PatientState) => U, listener: (selectedState: U, previousSelectedState: U) => void, options?: {
223
+ equalityFn?: ((a: U, b: U) => boolean) | undefined;
224
+ fireImmediately?: boolean;
225
+ } | undefined): () => void;
226
+ };
227
+ };
228
+ type PatientStore = ReturnType<typeof createPatientStore>;
229
+
230
+ interface SdkContextValue {
231
+ bid: string;
232
+ patientId: string;
233
+ store: PatientStore;
234
+ ready: boolean;
235
+ labelOf: DocumentTypeLabelResolver;
236
+ documentTypes: DocumentTypeConfig[];
237
+ }
238
+
239
+ declare function useSdk(): SdkContextValue;
240
+
241
+ /** True now that the Core SDK package is installed and wired in. */
242
+ declare const CORE_SDK_AVAILABLE = true;
243
+ /** Clear cached DB data for the bid then drop the client reference. */
244
+ declare function logoutMedicalRecords(bid?: string): Promise<void>;
245
+
246
+ interface RecordsViewProps {
247
+ className?: string;
248
+ onUpload?: () => void;
249
+ allowUpload?: boolean;
250
+ onCopyToNote?: (text: string, anchor?: {
251
+ x: number;
252
+ y: number;
253
+ }) => void | Promise<void>;
254
+ attachedIds?: string[];
255
+ onAttachManyToContext?: (records: Array<{
256
+ documentId: string;
257
+ name: string;
258
+ }>) => void | Promise<void>;
259
+ onRemoveAttachment?: (documentId: string) => void | Promise<void>;
260
+ onToast?: (message: string, type?: 'error' | 'success') => void;
261
+ }
262
+ declare function RecordsView({ className, onUpload, allowUpload, onCopyToNote, attachedIds, onAttachManyToContext, onRemoveAttachment, onToast, }: RecordsViewProps): react.JSX.Element;
263
+
264
+ interface RecordsConnectionActions {
265
+ refresh: () => Promise<void>;
266
+ /** Unix epoch ms of the last successful server refresh, or null before first call. */
267
+ sourceRefreshedAt: number | null;
268
+ editRecord: (documentId: string, data: EditRecordRequest) => Promise<void>;
269
+ deleteRecord: (documentId: string) => Promise<void>;
270
+ }
271
+ declare function useRecordsConnection(): RecordsConnectionActions;
272
+
273
+ interface CasesConnectionActions {
274
+ refresh: () => Promise<void>;
275
+ createCase: (data: Omit<CreateCaseRequest, 'id'> & {
276
+ id?: string;
277
+ }) => Promise<string>;
278
+ updateCase: (caseId: string, data: UpdateCaseRequest) => Promise<void>;
279
+ deleteCase: (caseId: string) => Promise<void>;
280
+ }
281
+ /**
282
+ * Loads cases for the current patient — but only when `enabled` (the Care
283
+ * Context tab is active), so the Date tab never triggers a cases fetch. Loads
284
+ * once per patient (cache-then-network via onStale); a `cases:changed`
285
+ * subscription keeps the store live while enabled.
286
+ */
287
+ declare function useCasesConnection(enabled?: boolean): CasesConnectionActions;
288
+
289
+ interface UploadFileInput {
290
+ file: File;
291
+ documentType: string;
292
+ documentDateMs: number;
293
+ caseIds?: string[];
294
+ tags?: string[];
295
+ }
296
+ interface UploadConnectionActions {
297
+ upload: (items: UploadFileInput[]) => Promise<void>;
298
+ }
299
+ declare function useUploadConnection(): UploadConnectionActions;
300
+
301
+ /**
302
+ * Tear down UI SDK state: drop the Core SDK client reference. The active patient
303
+ * store is owned by SdkProvider and disposed on unmount, so there's nothing else
304
+ * to clear here. (The Core SDK holds no persistent worker to shut down — auth
305
+ * lives in the host app.)
306
+ */
307
+ declare function useLogout(): () => void;
308
+
309
+ declare function useRecordById(id: string): RecordItem | undefined;
310
+ declare function useRecordIdRange(start: number, end: number): string[];
311
+ declare function useRecordsCount(): number;
312
+ declare function useRecordsStatus(): RecordsStatus;
313
+ declare function useThumbnailUrl(id: string): string | null;
314
+ declare function useIsSelected(id: string): boolean;
315
+ declare function useCaseById(id: string): CaseUiItem | undefined;
316
+ declare function useCaseIds(): string[];
317
+ declare function useCasesStatus(): RecordsStatus;
318
+ declare function useFilteredRecordIds(): string[];
319
+
320
+ interface SkeletonProps {
321
+ variant: 'card' | 'list-item' | 'text' | 'circle';
322
+ width?: number | string;
323
+ height?: number | string;
324
+ className?: string;
325
+ }
326
+ declare function Skeleton({ variant, width, height, className }: SkeletonProps): react.JSX.Element;
327
+
328
+ interface BadgeProps {
329
+ variant: 'smart' | 'analysing' | 'uploading' | 'upload_failure';
330
+ label: string;
331
+ }
332
+ declare const Badge: react.NamedExoticComponent<BadgeProps>;
333
+
334
+ interface TagChipProps {
335
+ label: string;
336
+ removable?: boolean;
337
+ onRemove?: () => void;
338
+ onClick?: (e: MouseEvent<HTMLSpanElement>) => void;
339
+ }
340
+ declare const TagChip: react.NamedExoticComponent<TagChipProps>;
341
+
342
+ interface IconButtonProps {
343
+ icon: ReactNode;
344
+ 'aria-label': string;
345
+ title?: string;
346
+ variant?: 'default' | 'primary' | 'solid' | 'ghost' | 'destructive';
347
+ disabled?: boolean;
348
+ onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
349
+ }
350
+ declare const IconButton: react.NamedExoticComponent<IconButtonProps>;
351
+
352
+ interface ThumbnailProps {
353
+ url: string | null;
354
+ size?: number;
355
+ alt?: string;
356
+ }
357
+ declare const Thumbnail: react.NamedExoticComponent<ThumbnailProps>;
358
+
359
+ interface FilterChipProps {
360
+ id: string;
361
+ label: string;
362
+ count?: number;
363
+ active: boolean;
364
+ onSelect: (id: string) => void;
365
+ }
366
+ declare const FilterChip: react.NamedExoticComponent<FilterChipProps>;
367
+
368
+ interface SidebarItemProps {
369
+ id: string;
370
+ dayNumber: string;
371
+ weekday: string;
372
+ countLabel: string;
373
+ summary: string;
374
+ selected: boolean;
375
+ onSelect: (id: string) => void;
376
+ }
377
+ declare const SidebarItem: react.NamedExoticComponent<SidebarItemProps>;
378
+
379
+ interface RecordCardProps extends RecordItemProps {
380
+ dense?: boolean;
381
+ }
382
+ declare const RecordCard: react.NamedExoticComponent<RecordCardProps>;
383
+
384
+ declare const RecordRow: react.NamedExoticComponent<RecordItemProps>;
385
+
386
+ declare function EmptyRecordsIcon({ size }: {
387
+ size?: number;
388
+ }): react.JSX.Element;
389
+
390
+ export { Badge, CORE_SDK_AVAILABLE, type CaseUiItem, type DocumentTypeConfig, EmptyRecordsIcon, type Environment, FilterChip, IconButton, RecordCard, type RecordItem, RecordRow, RecordsView, type RecordsViewProps, type SdkConfig, SdkProvider, type SdkProviderProps, SidebarItem, Skeleton, TagChip, Thumbnail, logoutMedicalRecords, useCaseById, useCaseIds, useCasesConnection, useCasesStatus, useFilteredRecordIds, useIsSelected, useLogout, useRecordById, useRecordIdRange, useRecordsConnection, useRecordsCount, useRecordsStatus, useSdk, useThumbnailUrl, useUploadConnection };