@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,154 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { Checkbox, Tile, Tag, Layer } from '@carbon/react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { showSnackbar, useLayoutType, EmptyCardIllustration } from '@openmrs/esm-framework';
6
+ import { type Task, useTaskList, setTaskStatusCompleted, getPriorityLabel } from './task-list.resource';
7
+ import Loader from '../loader/loader.component';
8
+ import styles from './task-list-view.scss';
9
+
10
+ export interface TaskListViewProps {
11
+ patientUuid: string;
12
+ onTaskClick?: (task: Task) => void;
13
+ }
14
+
15
+ const TaskListView: React.FC<TaskListViewProps> = ({ patientUuid, onTaskClick }) => {
16
+ const { t } = useTranslation();
17
+ const { tasks, isLoading, error, mutate } = useTaskList(patientUuid);
18
+ const isTablet = useLayoutType() === 'tablet';
19
+ const [pendingUpdates, setPendingUpdates] = useState<Set<string>>(new Set());
20
+
21
+ const addPendingUpdate = useCallback((uuid: string) => {
22
+ setPendingUpdates((current) => {
23
+ const next = new Set(current);
24
+ next.add(uuid);
25
+ return next;
26
+ });
27
+ }, []);
28
+
29
+ const removePendingUpdate = useCallback((uuid: string) => {
30
+ setPendingUpdates((current) => {
31
+ const next = new Set(current);
32
+ next.delete(uuid);
33
+ return next;
34
+ });
35
+ }, []);
36
+
37
+ const handleToggle = useCallback(
38
+ async (task: Task, checked: boolean) => {
39
+ addPendingUpdate(task.uuid);
40
+ try {
41
+ await setTaskStatusCompleted(patientUuid, task, checked);
42
+ await mutate();
43
+ } catch (_error) {
44
+ showSnackbar({
45
+ title: t('taskUpdateFailed', 'Unable to update task'),
46
+ kind: 'error',
47
+ });
48
+ } finally {
49
+ removePendingUpdate(task.uuid);
50
+ }
51
+ },
52
+ [addPendingUpdate, mutate, patientUuid, removePendingUpdate, t],
53
+ );
54
+
55
+ const isOverdue = useCallback((task: Task) => {
56
+ if (task.completed || !task.dueDate) {
57
+ return false;
58
+ }
59
+ const dueDate = task.dueDate.date;
60
+ if (!dueDate) {
61
+ return false;
62
+ }
63
+ const now = new Date();
64
+ // Create new Date objects for comparison without mutating originals
65
+ const nowDateOnly = new Date(now.getFullYear(), now.getMonth(), now.getDate());
66
+ const dueDateOnly = new Date(dueDate.getFullYear(), dueDate.getMonth(), dueDate.getDate());
67
+ return dueDateOnly < nowDateOnly;
68
+ }, []);
69
+
70
+ if (isLoading) {
71
+ return <Loader />;
72
+ }
73
+
74
+ if (error) {
75
+ return <p className={styles.errorText}>{t('taskLoadError', 'There was a problem loading the task list.')}</p>;
76
+ }
77
+
78
+ if (!tasks || tasks.length === 0) {
79
+ return (
80
+ <Layer>
81
+ <Tile className={styles.emptyStateTile}>
82
+ <div className={styles.emptyStateTileContent}>
83
+ <EmptyCardIllustration />
84
+ <p className={styles.emptyStateContent}>{t('noTasksMessage', 'No tasks to display')}</p>
85
+ </div>
86
+ </Tile>
87
+ </Layer>
88
+ );
89
+ }
90
+
91
+ return (
92
+ <ul className={styles.taskList}>
93
+ {tasks.map((task) => {
94
+ const isUpdating = pendingUpdates.has(task.uuid);
95
+ const overdue = isOverdue(task);
96
+ const assigneeDisplay = task.assignee
97
+ ? (task.assignee.display ?? task.assignee.uuid)
98
+ : t('noAssignment', 'No assignment');
99
+
100
+ return (
101
+ <li key={task.uuid}>
102
+ <Tile
103
+ role="listitem"
104
+ className={classNames(styles.taskTile, {
105
+ [styles.tabletTaskTile]: isTablet,
106
+ [styles.completedTile]: task.completed,
107
+ })}
108
+ >
109
+ <div className={styles.taskTileCheckbox}>
110
+ <div
111
+ className={classNames(styles.checkboxWrapper, {
112
+ [styles.completedCheckbox]: task.completed,
113
+ })}
114
+ >
115
+ <Checkbox
116
+ id={`task-${task.uuid}`}
117
+ labelText=""
118
+ checked={task.completed}
119
+ disabled={isUpdating}
120
+ onChange={(_, { checked }) => handleToggle(task, checked)}
121
+ />
122
+ </div>
123
+ </div>
124
+ <button onClick={() => onTaskClick?.(task)} className={styles.taskTileContentButton}>
125
+ <div className={styles.taskNameWrapper}>
126
+ <span>{task.name}</span>
127
+ {task.rationale && <div className={styles.taskRationalePreview}>{task.rationale}</div>}
128
+ <div className={styles.taskAssignee}>{assigneeDisplay}</div>
129
+ </div>
130
+ <div className={styles.taskTags}>
131
+ {task.priority && (
132
+ <Tag
133
+ type={task.priority === 'high' ? 'red' : task.priority === 'medium' ? 'gray' : 'green'}
134
+ size="sm"
135
+ >
136
+ {getPriorityLabel(task.priority, t)}
137
+ </Tag>
138
+ )}
139
+ {overdue && (
140
+ <Tag type="red" size="sm">
141
+ {t('overdue', 'Overdue')}
142
+ </Tag>
143
+ )}
144
+ </div>
145
+ </button>
146
+ </Tile>
147
+ </li>
148
+ );
149
+ })}
150
+ </ul>
151
+ );
152
+ };
153
+
154
+ export default TaskListView;
@@ -0,0 +1,150 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+ @use '@openmrs/esm-styleguide/src/vars' as *;
5
+
6
+ .taskList {
7
+ padding: layout.$spacing-03;
8
+ display: grid;
9
+ gap: layout.$spacing-02;
10
+ }
11
+
12
+ .helperText {
13
+ margin: layout.$spacing-05 0;
14
+ color: $interactive-01;
15
+ }
16
+
17
+ .errorText {
18
+ margin: layout.$spacing-05 0;
19
+ }
20
+
21
+ .taskTile {
22
+ padding: 0;
23
+ border: 1px solid $grey-2;
24
+ display: flex;
25
+ flex-direction: row;
26
+ }
27
+
28
+ .tabletTaskTile {
29
+ &:not(:last-of-type) {
30
+ margin-bottom: layout.$spacing-04;
31
+ }
32
+ }
33
+
34
+ .taskTileCheckbox {
35
+ padding: layout.$spacing-03 0 layout.$spacing-03 layout.$spacing-05;
36
+ display: flex;
37
+ align-items: flex-start;
38
+ gap: layout.$spacing-03;
39
+ min-width: 0;
40
+ }
41
+
42
+ .taskTileContentButton {
43
+ padding: layout.$spacing-03 layout.$spacing-05;
44
+ display: flex;
45
+ align-items: flex-start;
46
+ gap: layout.$spacing-03;
47
+ min-width: 0;
48
+ border: none;
49
+ background-color: transparent;
50
+ text-align: left;
51
+ flex: 1;
52
+ transition: background-color 1s ease-in-out;
53
+ }
54
+
55
+ .taskTileContentButton:hover {
56
+ background-color: rgba(colors.$gray-100, 0.03);
57
+ }
58
+
59
+ .completedTile {
60
+ opacity: 0.65;
61
+ }
62
+
63
+ .taskNameWrapper {
64
+ display: flex;
65
+ flex-direction: column;
66
+ gap: layout.$spacing-02;
67
+ flex: 1;
68
+ min-width: 0;
69
+ }
70
+
71
+ .taskRationalePreview {
72
+ font-size: 0.875rem;
73
+ color: $text-02;
74
+ line-height: 1.4;
75
+ }
76
+
77
+ .taskAssignee {
78
+ @include type.type-style('label-01');
79
+ color: $text-02;
80
+ }
81
+
82
+ .taskTags {
83
+ display: flex;
84
+ gap: layout.$spacing-02;
85
+ flex-shrink: 0;
86
+ }
87
+
88
+ .checkboxWrapper {
89
+ :global(.cds--checkbox-label) {
90
+ font-weight: 600;
91
+ }
92
+ }
93
+
94
+ .completedCheckbox {
95
+ :global(.cds--checkbox-label) {
96
+ text-decoration: line-through;
97
+ color: $text-02;
98
+ }
99
+ }
100
+
101
+ .dueDate {
102
+ font-size: 0.875rem;
103
+ color: $text-02;
104
+ white-space: nowrap;
105
+ }
106
+
107
+ .taskBody {
108
+ display: grid;
109
+ gap: layout.$spacing-03;
110
+ color: $text-02;
111
+ font-size: 0.875rem;
112
+ }
113
+
114
+ .taskMeta {
115
+ display: flex;
116
+ flex-wrap: wrap;
117
+ column-gap: layout.$spacing-02;
118
+ row-gap: layout.$spacing-01;
119
+ }
120
+
121
+ .metaLabel {
122
+ font-weight: 600;
123
+ }
124
+
125
+ .taskRationale {
126
+ display: grid;
127
+ gap: layout.$spacing-02;
128
+
129
+ p {
130
+ margin: 0;
131
+ line-height: 1.4;
132
+ }
133
+ }
134
+
135
+ .emptyStateTile {
136
+ padding: layout.$spacing-06 0;
137
+ }
138
+
139
+ .emptyStateTileContent {
140
+ display: flex;
141
+ flex-direction: column;
142
+ align-items: center;
143
+ }
144
+
145
+ .emptyStateContent {
146
+ @include type.type-style('heading-compact-01');
147
+ color: colors.$gray-70;
148
+ margin-top: layout.$spacing-05;
149
+ margin-bottom: layout.$spacing-03;
150
+ }