@openmrs/esm-task-list-app 1.0.0

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.
Files changed (42) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +57 -0
  4. package/.husky/pre-commit +7 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +13 -0
  7. package/.turbo.json +18 -0
  8. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
  9. package/LICENSE +401 -0
  10. package/README.md +28 -0
  11. package/__mocks__/react-i18next.js +73 -0
  12. package/example.env +6 -0
  13. package/jest.config.js +34 -0
  14. package/package.json +108 -0
  15. package/prettier.config.js +8 -0
  16. package/src/config-schema.ts +13 -0
  17. package/src/declarations.d.ts +5 -0
  18. package/src/index.ts +24 -0
  19. package/src/launch-button/task-list-launch-button.extension.tsx +20 -0
  20. package/src/loader/loader.component.tsx +12 -0
  21. package/src/loader/loader.scss +9 -0
  22. package/src/routes.json +28 -0
  23. package/src/types.d.ts +9 -0
  24. package/src/workspace/add-task-form.component.tsx +551 -0
  25. package/src/workspace/add-task-form.scss +58 -0
  26. package/src/workspace/add-task-form.test.tsx +458 -0
  27. package/src/workspace/delete-task.modal.tsx +71 -0
  28. package/src/workspace/delete-task.scss +7 -0
  29. package/src/workspace/task-details-view.component.tsx +212 -0
  30. package/src/workspace/task-details-view.scss +67 -0
  31. package/src/workspace/task-details-view.test.tsx +411 -0
  32. package/src/workspace/task-list-view.component.tsx +154 -0
  33. package/src/workspace/task-list-view.scss +150 -0
  34. package/src/workspace/task-list.resource.ts +570 -0
  35. package/src/workspace/task-list.scss +37 -0
  36. package/src/workspace/task-list.workspace.tsx +88 -0
  37. package/tools/i18next-parser.config.js +89 -0
  38. package/tools/setup-tests.ts +8 -0
  39. package/tools/update-openmrs-deps.mjs +43 -0
  40. package/translations/en.json +63 -0
  41. package/tsconfig.json +24 -0
  42. package/webpack.config.js +1 -0
@@ -0,0 +1,570 @@
1
+ import { useMemo, useState } from 'react';
2
+ import useSWR from 'swr';
3
+ import useSWRImmutable from 'swr/immutable';
4
+ import { type TFunction } from 'i18next';
5
+ import {
6
+ type FetchResponse,
7
+ openmrsFetch,
8
+ restBaseUrl,
9
+ parseDate,
10
+ useDebounce,
11
+ useConfig,
12
+ } from '@openmrs/esm-framework';
13
+ import { type CarePlan } from '../types';
14
+
15
+ export interface Assignee {
16
+ uuid: string;
17
+ display?: string;
18
+ type: 'person' | 'role';
19
+ }
20
+
21
+ export type DueDateType = 'THIS_VISIT' | 'NEXT_VISIT' | 'DATE';
22
+
23
+ export type Priority = 'high' | 'medium' | 'low';
24
+
25
+ export function getPriorityLabel(priority: Priority, t: TFunction): string {
26
+ const labels: Record<Priority, string> = {
27
+ high: t('priorityHigh', 'High'),
28
+ medium: t('priorityMedium', 'Medium'),
29
+ low: t('priorityLow', 'Low'),
30
+ };
31
+ return labels[priority];
32
+ }
33
+
34
+ export interface Task {
35
+ uuid: string;
36
+ name: string;
37
+ status?: string;
38
+ dueDate?: TaskDueDate;
39
+ createdDate: Date;
40
+ rationale?: string;
41
+ assignee?: Assignee;
42
+ createdBy?: string;
43
+ completed: boolean;
44
+ priority?: Priority;
45
+ systemTaskUuid?: string;
46
+ }
47
+
48
+ export type TaskDueDate = TaskDueDateDate | TaskDueDateVisit;
49
+
50
+ export type TaskDueDateDate = {
51
+ type: Extract<DueDateType, 'DATE'>;
52
+ date: Date;
53
+ };
54
+
55
+ export type TaskDueDateVisit = {
56
+ type: Extract<DueDateType, 'THIS_VISIT' | 'NEXT_VISIT'>;
57
+ referenceVisitUuid?: string;
58
+ date?: Date;
59
+ };
60
+
61
+ export interface TaskInput {
62
+ name: string;
63
+ dueDate?: TaskDueDate;
64
+ rationale?: string;
65
+ assignee?: Assignee;
66
+ priority?: Priority;
67
+ systemTaskUuid?: string;
68
+ }
69
+
70
+ export interface FHIRCarePlanResponse {
71
+ entry: Array<{
72
+ resource: CarePlan;
73
+ }>;
74
+ }
75
+
76
+ export interface SelectOption {
77
+ id: string;
78
+ label?: string;
79
+ }
80
+
81
+ export interface ProviderSearchResponse {
82
+ results: Array<{
83
+ uuid: string;
84
+ display: string;
85
+ }>;
86
+ }
87
+
88
+ export interface ProviderRoleSearchResponse {
89
+ results: Array<{
90
+ uuid: string;
91
+ name: string;
92
+ }>;
93
+ }
94
+
95
+ const carePlanEndpoint = `${restBaseUrl}/tasks/careplan`;
96
+
97
+ export function taskListSWRKey(patientUuid: string) {
98
+ return `${carePlanEndpoint}?subject=Patient/${patientUuid}`;
99
+ }
100
+
101
+ export function useTaskList(patientUuid: string) {
102
+ const swrKey = taskListSWRKey(patientUuid);
103
+ const { data, isLoading, error, mutate } = useSWR<{ data: FHIRCarePlanResponse }>(swrKey, openmrsFetch);
104
+
105
+ const tasks = useMemo(() => {
106
+ const parsedTasks = data?.data?.entry?.map((entry) => createTaskFromCarePlan(entry.resource)) ?? [];
107
+ const validTasks = parsedTasks.filter((task) => Boolean(task.uuid));
108
+ const activeTasks = validTasks.filter((task) => task.status !== 'cancelled');
109
+
110
+ return activeTasks.sort((a, b) => {
111
+ if (a.completed !== b.completed) {
112
+ return a.completed ? 1 : -1;
113
+ }
114
+
115
+ const aDue = a.dueDate?.date?.getTime() ?? 0;
116
+ const bDue = b.dueDate?.date?.getTime() ?? 0;
117
+
118
+ return aDue - bDue;
119
+ });
120
+ }, [data]);
121
+
122
+ return { tasks, isLoading, error, mutate };
123
+ }
124
+
125
+ export function saveTask(patientUuid: string, task: TaskInput) {
126
+ const carePlan = buildCarePlan(patientUuid, task);
127
+
128
+ return openmrsFetch(carePlanEndpoint, {
129
+ headers: {
130
+ 'Content-Type': 'application/json',
131
+ },
132
+ method: 'POST',
133
+ body: JSON.stringify(carePlan),
134
+ });
135
+ }
136
+
137
+ export function updateTask(patientUuid: string, task: Task) {
138
+ const carePlan = buildCarePlan(patientUuid, task);
139
+
140
+ return openmrsFetch(`${carePlanEndpoint}/${task.uuid}`, {
141
+ headers: {
142
+ 'Content-Type': 'application/json',
143
+ },
144
+ method: 'PUT',
145
+ body: JSON.stringify(carePlan),
146
+ });
147
+ }
148
+
149
+ export function setTaskStatusCompleted(patientUuid: string, task: Task, completed: boolean) {
150
+ const status = completed ? 'completed' : task.status && task.status !== 'completed' ? task.status : 'in-progress';
151
+
152
+ return updateTask(patientUuid, {
153
+ ...task,
154
+ completed,
155
+ status,
156
+ });
157
+ }
158
+
159
+ export function useTask(taskUuid: string) {
160
+ const swrKey = taskUuid ? `${carePlanEndpoint}/${taskUuid}` : null;
161
+ const { data, isLoading, error, mutate } = useSWR<{ data: CarePlan }>(swrKey, openmrsFetch);
162
+
163
+ const task = useMemo(() => {
164
+ if (!data?.data) {
165
+ return null;
166
+ }
167
+ return createTaskFromCarePlan(data.data);
168
+ }, [data]);
169
+
170
+ return { task, isLoading, error, mutate };
171
+ }
172
+
173
+ export function deleteTask(patientUuid: string, task: Task) {
174
+ return updateTask(patientUuid, {
175
+ ...task,
176
+ status: 'cancelled',
177
+ });
178
+ }
179
+
180
+ function createTaskFromCarePlan(carePlan: CarePlan): Task {
181
+ const activity = carePlan?.activity?.[0];
182
+ const detail = activity?.detail;
183
+
184
+ const status = detail?.status;
185
+
186
+ const performers = detail?.performer ?? [];
187
+ const { dueDate, dueDateType } = extractDueDate(detail);
188
+ const priority = extractPriority(detail);
189
+ const createdBy = carePlan?.author?.display;
190
+ const systemTaskUuid = extractSystemTaskUuid(carePlan.instantiatesCanonical);
191
+
192
+ const task: Task = {
193
+ uuid: carePlan.id ?? '',
194
+ name: detail?.description ?? '',
195
+ status,
196
+ createdDate: parseDate(carePlan.created),
197
+ dueDate: {
198
+ type: dueDateType as 'DATE' | 'THIS_VISIT' | 'NEXT_VISIT',
199
+ date: dueDate,
200
+ },
201
+ rationale: carePlan.description ?? undefined,
202
+ createdBy,
203
+ completed: status === 'completed',
204
+ priority,
205
+ systemTaskUuid,
206
+ };
207
+
208
+ performers.forEach((performer) => {
209
+ const assignment = parseAssignment(performer);
210
+ if (!assignment) {
211
+ return;
212
+ }
213
+ if (assignment.type === 'provider') {
214
+ task.assignee = assignment.value;
215
+ }
216
+ if (assignment.type === 'providerRole') {
217
+ task.assignee = assignment.value;
218
+ }
219
+ });
220
+
221
+ return task;
222
+ }
223
+
224
+ function extractSystemTaskUuid(instantiatesCanonical?: string[]): string | undefined {
225
+ if (!instantiatesCanonical || instantiatesCanonical.length === 0) {
226
+ return undefined;
227
+ }
228
+
229
+ for (const reference of instantiatesCanonical) {
230
+ if (reference.startsWith('PlanDefinition/')) {
231
+ return reference.substring('PlanDefinition/'.length);
232
+ }
233
+ }
234
+
235
+ return undefined;
236
+ }
237
+
238
+ function buildCarePlan(patientUuid: string, task: Partial<Task>) {
239
+ const performer: Array<fhir.Reference> = [];
240
+
241
+ if (task.assignee?.type === 'role' && task.assignee?.uuid) {
242
+ performer.push({
243
+ reference: `PractitionerRole/${task.assignee.uuid}`,
244
+ display: task.assignee.display,
245
+ });
246
+ } else if (task.assignee?.uuid) {
247
+ performer.push({
248
+ reference: `Practitioner/${task.assignee.uuid}`,
249
+ display: task.assignee.display,
250
+ });
251
+ }
252
+
253
+ const detail: fhir.CarePlanActivityDetail = {
254
+ status: task.status ?? 'not-started',
255
+ description: task.name,
256
+ };
257
+
258
+ if (performer.length > 0) {
259
+ detail.performer = performer;
260
+ }
261
+
262
+ // Handle due date. Due date is stored as scheduledPeriod. The type of visit is stored
263
+ // using an activity-dueKind extension. For visit-based due dates, the visit UUID is stored in an
264
+ // encounter-associatedEncounter extension.
265
+ detail.extension = detail.extension || [];
266
+
267
+ if (task.dueDate?.type === 'THIS_VISIT' || task.dueDate?.type === 'NEXT_VISIT') {
268
+ // Visit-based types: use scheduledPeriod (end date set server-side if visit ended)
269
+ detail.scheduledPeriod = {};
270
+
271
+ detail.extension.push({
272
+ url: 'http://openmrs.org/fhir/StructureDefinition/activity-dueKind',
273
+ valueCode: task.dueDate?.type === 'THIS_VISIT' ? 'this-visit' : 'next-visit',
274
+ });
275
+
276
+ if (task.dueDate?.referenceVisitUuid) {
277
+ detail.extension.push({
278
+ url: 'http://hl7.org/fhir/StructureDefinition/encounter-associatedEncounter',
279
+ valueReference: {
280
+ reference: `Encounter/${task.dueDate.referenceVisitUuid}`,
281
+ },
282
+ });
283
+ }
284
+ } else if (task.dueDate?.type === 'DATE' && task.dueDate?.date) {
285
+ detail.scheduledPeriod = {
286
+ end: task.dueDate.date.toISOString(),
287
+ };
288
+
289
+ detail.extension.push({
290
+ url: 'http://openmrs.org/fhir/StructureDefinition/activity-dueKind',
291
+ valueCode: 'date',
292
+ });
293
+ }
294
+
295
+ // Add priority extension if set
296
+ if (task.priority) {
297
+ detail.extension.push({
298
+ url: 'http://openmrs.org/fhir/StructureDefinition/activity-priority',
299
+ valueCode: task.priority,
300
+ });
301
+ }
302
+
303
+ const carePlan: CarePlan = {
304
+ resourceType: 'CarePlan',
305
+ status: task.completed ? 'completed' : 'active',
306
+ intent: 'plan',
307
+ description: task.rationale,
308
+ subject: {
309
+ reference: `Patient/${patientUuid}`,
310
+ },
311
+ activity: [
312
+ {
313
+ detail,
314
+ },
315
+ ],
316
+ };
317
+
318
+ if (task.uuid) {
319
+ carePlan.id = task.uuid;
320
+ }
321
+
322
+ if (task.systemTaskUuid) {
323
+ carePlan.instantiatesCanonical = [`PlanDefinition/${task.systemTaskUuid}`];
324
+ }
325
+
326
+ return carePlan;
327
+ }
328
+
329
+ function parseAssignment(
330
+ performer: fhir.Reference,
331
+ ): { type: 'provider'; value: Assignee } | { type: 'providerRole'; value: Assignee } | undefined {
332
+ if (!performer) {
333
+ return undefined;
334
+ }
335
+
336
+ const reference = performer.reference ?? '';
337
+ const [resourceType, identifier] = reference.split('/');
338
+
339
+ if (resourceType === 'Practitioner' && identifier) {
340
+ return {
341
+ type: 'provider',
342
+ value: {
343
+ uuid: identifier,
344
+ display: performer.display ?? undefined,
345
+ type: 'person',
346
+ },
347
+ };
348
+ }
349
+
350
+ if (resourceType === 'PractitionerRole' && identifier) {
351
+ return {
352
+ type: 'providerRole',
353
+ value: {
354
+ uuid: identifier,
355
+ display: performer.display ?? undefined,
356
+ type: 'role',
357
+ },
358
+ };
359
+ }
360
+
361
+ console.warn('Unknown performer type', performer);
362
+ return null;
363
+ }
364
+
365
+ function extractDueDate(detail?: fhir.CarePlanActivityDetail): {
366
+ dueDate?: Date;
367
+ dueDateCreatedDate?: Date;
368
+ dueDateType?: DueDateType;
369
+ } {
370
+ if (!detail) {
371
+ return {};
372
+ }
373
+
374
+ // Read due date type from activity-dueKind extension
375
+ let dueDateType: DueDateType | undefined;
376
+ let visitUuid: string | undefined;
377
+
378
+ if (detail.extension) {
379
+ for (const ext of detail.extension) {
380
+ if (ext.url === 'http://openmrs.org/fhir/StructureDefinition/activity-dueKind') {
381
+ const value = (ext as any).valueCode || (ext as any).valueString;
382
+ if (value === 'this-visit') {
383
+ dueDateType = 'THIS_VISIT';
384
+ } else if (value === 'next-visit') {
385
+ dueDateType = 'NEXT_VISIT';
386
+ } else if (value === 'date') {
387
+ dueDateType = 'DATE';
388
+ }
389
+ } else if (ext.url === 'http://hl7.org/fhir/StructureDefinition/encounter-associatedEncounter') {
390
+ const ref = (ext as any).valueReference?.reference;
391
+ if (ref && ref.startsWith('Encounter/')) {
392
+ visitUuid = ref.substring('Encounter/'.length);
393
+ }
394
+ }
395
+ }
396
+ }
397
+
398
+ // Read due date from scheduledPeriod.end (always present if date is known)
399
+ let dueDate: string | undefined;
400
+ if (detail.scheduledPeriod?.end) {
401
+ dueDate = detail.scheduledPeriod.end;
402
+ // If no dueKind extension was found but we have an end date, assume DATE type
403
+ if (!dueDateType) {
404
+ dueDateType = 'DATE';
405
+ }
406
+ }
407
+
408
+ // For visit-based types, dueDate will be null/undefined if visit hasn't ended
409
+ // For DATE type, dueDate will contain the actual date
410
+
411
+ return {
412
+ dueDate: dueDate ? parseDate(dueDate) : undefined,
413
+ dueDateType,
414
+ };
415
+ }
416
+
417
+ function extractPriority(detail?: fhir.CarePlanActivityDetail): Priority | undefined {
418
+ if (!detail?.extension) {
419
+ return undefined;
420
+ }
421
+
422
+ for (const ext of detail.extension) {
423
+ if (ext.url === 'http://openmrs.org/fhir/StructureDefinition/activity-priority') {
424
+ const value = (ext as any).valueCode || (ext as any).valueString;
425
+ if (value === 'high' || value === 'medium' || value === 'low') {
426
+ return value;
427
+ }
428
+ }
429
+ }
430
+
431
+ return undefined;
432
+ }
433
+
434
+ export function useFetchProviders() {
435
+ const [query, setQuery] = useState<string>('');
436
+ const debouncedQuery = useDebounce(query, 300);
437
+ const url =
438
+ debouncedQuery.length < 2
439
+ ? null
440
+ : `${restBaseUrl}/provider?q=${encodeURIComponent(debouncedQuery)}&v=custom:(uuid,display)`;
441
+ const { data, isLoading, error } = useSWR<FetchResponse<ProviderSearchResponse>>(url, openmrsFetch);
442
+
443
+ return {
444
+ providers: data?.data?.results ?? [],
445
+ setProviderQuery: setQuery,
446
+ isLoading,
447
+ error,
448
+ };
449
+ }
450
+
451
+ export function useProviderRoles() {
452
+ const { allowAssigningProviderRole } = useConfig();
453
+ const url = allowAssigningProviderRole ? `${restBaseUrl}/providerrole?v=custom:(uuid,name)` : null;
454
+ const response = useSWRImmutable<FetchResponse<ProviderRoleSearchResponse>>(url, openmrsFetch);
455
+ const results = response?.data?.data?.results ?? [];
456
+ return results.map((result) => ({
457
+ id: result.uuid,
458
+ label: result.name,
459
+ }));
460
+ }
461
+
462
+ export function useReferenceVisit(dueDateType: string, patientUuid: string) {
463
+ const referenceVisitUrl =
464
+ dueDateType === 'NEXT_VISIT'
465
+ ? `${restBaseUrl}/visit?patient=${patientUuid}&v=custom:(uuid)&includeInactive=true&limit=1`
466
+ : null;
467
+ const {
468
+ data: referenceVisitResponse,
469
+ isLoading: isReferenceVisitLoading,
470
+ error: referenceVisitError,
471
+ } = useSWR<FetchResponse<{ results: Array<{ uuid: string }> }>>(referenceVisitUrl, openmrsFetch);
472
+ return {
473
+ data: referenceVisitResponse?.data,
474
+ isLoading: isReferenceVisitLoading,
475
+ error: referenceVisitError,
476
+ };
477
+ }
478
+
479
+ // PlanDefinition types for system tasks
480
+ export interface PlanDefinition {
481
+ resourceType: 'PlanDefinition';
482
+ id: string;
483
+ name?: string;
484
+ title?: string;
485
+ status?: string;
486
+ description?: string;
487
+ action?: Array<{
488
+ title?: string;
489
+ reason?: Array<{ text?: string }>;
490
+ participant?: Array<{
491
+ role?: { coding?: Array<{ code?: string; display?: string }> };
492
+ }>;
493
+ extension?: Array<{
494
+ url: string;
495
+ valueCode?: string;
496
+ }>;
497
+ }>;
498
+ }
499
+
500
+ export interface FHIRPlanDefinitionBundle {
501
+ resourceType: 'Bundle';
502
+ entry?: Array<{
503
+ resource: PlanDefinition;
504
+ }>;
505
+ }
506
+
507
+ export interface SystemTask {
508
+ uuid: string;
509
+ name: string;
510
+ title: string;
511
+ description?: string;
512
+ priority?: Priority;
513
+ defaultAssigneeRoleUuid?: string;
514
+ defaultAssigneeRoleDisplay?: string;
515
+ rationale?: string;
516
+ }
517
+
518
+ const fhirBaseUrl = '/ws/fhir2/R4';
519
+
520
+ function planDefinitionToSystemTask(pd: PlanDefinition): SystemTask {
521
+ const action = pd.action?.[0];
522
+
523
+ // Extract priority from action extension
524
+ let priority: Priority | undefined;
525
+ if (action?.extension) {
526
+ for (const ext of action.extension) {
527
+ if (ext.url === 'http://openmrs.org/fhir/StructureDefinition/activity-priority') {
528
+ const value = ext.valueCode;
529
+ if (value === 'high' || value === 'medium' || value === 'low') {
530
+ priority = value;
531
+ }
532
+ }
533
+ }
534
+ }
535
+
536
+ // Extract default assignee role from action participant
537
+ let defaultAssigneeRoleUuid: string | undefined;
538
+ let defaultAssigneeRoleDisplay: string | undefined;
539
+ if (action?.participant?.[0]?.role?.coding?.[0]) {
540
+ const coding = action.participant[0].role.coding[0];
541
+ defaultAssigneeRoleUuid = coding.code;
542
+ defaultAssigneeRoleDisplay = coding.display;
543
+ }
544
+
545
+ // Extract rationale from action reason
546
+ const rationale = action?.reason?.[0]?.text;
547
+
548
+ return {
549
+ uuid: pd.id,
550
+ name: pd.name ?? '',
551
+ title: pd.title ?? pd.name ?? '',
552
+ description: pd.description,
553
+ priority,
554
+ defaultAssigneeRoleUuid,
555
+ defaultAssigneeRoleDisplay,
556
+ rationale,
557
+ };
558
+ }
559
+
560
+ export function useSystemTasks() {
561
+ const url = `${fhirBaseUrl}/PlanDefinition?status=active`;
562
+ const { data, isLoading, error } = useSWRImmutable<FetchResponse<FHIRPlanDefinitionBundle>>(url, openmrsFetch);
563
+
564
+ const systemTasks = useMemo(() => {
565
+ const entries = data?.data?.entry ?? [];
566
+ return entries.map((entry) => planDefinitionToSystemTask(entry.resource));
567
+ }, [data]);
568
+
569
+ return { systemTasks, isLoading, error };
570
+ }
@@ -0,0 +1,37 @@
1
+ @use '@carbon/layout';
2
+ @use '@openmrs/esm-styleguide/src/vars' as *;
3
+
4
+ .workspaceContainer {
5
+ display: flex;
6
+ flex-direction: column;
7
+ height: 100%;
8
+ }
9
+
10
+ .addTaskButtonContainer {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ color: $ui-02;
15
+ }
16
+
17
+ .backButton {
18
+ padding: 0 layout.$spacing-05;
19
+
20
+ button {
21
+ display: flex;
22
+
23
+ svg {
24
+ order: 1;
25
+ margin-right: layout.$spacing-05;
26
+ margin-left: 0 !important;
27
+ fill: currentColor !important;
28
+ }
29
+
30
+ span {
31
+ order: 2;
32
+
33
+ // Center text vertically within span
34
+ line-height: 2em;
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,88 @@
1
+ import React, { useState } from 'react';
2
+ import { Button } from '@carbon/react';
3
+ import { Add, ArrowLeft } from '@carbon/react/icons';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { type DefaultWorkspaceProps } from '@openmrs/esm-framework';
6
+ import { type Task } from './task-list.resource';
7
+ import AddTaskForm from './add-task-form.component';
8
+ import TaskListView from './task-list-view.component';
9
+ import TaskDetailsView from './task-details-view.component';
10
+ import styles from './task-list.scss';
11
+
12
+ type View = 'list' | 'form' | 'details' | 'edit';
13
+
14
+ const TaskListWorkspace: React.FC<DefaultWorkspaceProps & { patientUuid: string }> = ({ patientUuid }) => {
15
+ const { t } = useTranslation();
16
+ const [view, setView] = useState<View>('list');
17
+ const [selectedTaskUuid, setSelectedTaskUuid] = useState<string | null>(null);
18
+
19
+ const handleTaskClick = (task: Task) => {
20
+ setSelectedTaskUuid(task.uuid);
21
+ setView('details');
22
+ };
23
+
24
+ const handleEdit = (task: Task) => {
25
+ setSelectedTaskUuid(task.uuid);
26
+ setView('edit');
27
+ };
28
+
29
+ const handleEditComplete = () => {
30
+ setView('details');
31
+ };
32
+
33
+ const handleBackClick = () => {
34
+ if (view === 'edit') {
35
+ setView('details');
36
+ return;
37
+ }
38
+ setView('list');
39
+ setSelectedTaskUuid(null);
40
+ };
41
+
42
+ const backText =
43
+ view === 'edit' ? t('backToTaskDetails', 'Back to task details') : t('backToTaskList', 'Back to task list');
44
+
45
+ return (
46
+ <div className={styles.workspaceContainer}>
47
+ {['form', 'details', 'edit'].includes(view) && (
48
+ <div className={styles.backButton}>
49
+ <Button
50
+ kind="ghost"
51
+ renderIcon={(props) => <ArrowLeft size={16} {...props} />}
52
+ iconDescription={backText}
53
+ onClick={handleBackClick}
54
+ >
55
+ <span>{backText}</span>
56
+ </Button>
57
+ </div>
58
+ )}
59
+ {view === 'form' && <AddTaskForm patientUuid={patientUuid} onBack={() => setView('list')} />}
60
+ {view === 'list' && <TaskListView patientUuid={patientUuid} onTaskClick={handleTaskClick} />}
61
+ {view === 'list' && (
62
+ <div className={styles.addTaskButtonContainer}>
63
+ <Button
64
+ kind="ghost"
65
+ renderIcon={(props) => <Add size={16} {...props} />}
66
+ iconDescription={t('addTask', 'Add Task')}
67
+ onClick={() => setView('form')}
68
+ >
69
+ {t('addTask', 'Add Task')}
70
+ </Button>
71
+ </div>
72
+ )}
73
+ {view === 'details' && selectedTaskUuid && (
74
+ <TaskDetailsView
75
+ patientUuid={patientUuid}
76
+ taskUuid={selectedTaskUuid}
77
+ onBack={handleBackClick}
78
+ onEdit={handleEdit}
79
+ />
80
+ )}
81
+ {view === 'edit' && selectedTaskUuid && (
82
+ <AddTaskForm patientUuid={patientUuid} onBack={handleEditComplete} editTaskUuid={selectedTaskUuid} />
83
+ )}
84
+ </div>
85
+ );
86
+ };
87
+
88
+ export default TaskListWorkspace;