@openmrs/esm-bed-management-app 9.2.1-pre.7327 → 9.2.1-pre.7339

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.
@@ -19,14 +19,15 @@ import {
19
19
  import { z } from 'zod';
20
20
  import { zodResolver } from '@hookform/resolvers/zod';
21
21
  import {
22
- type DefaultWorkspaceProps,
23
22
  getCoreTranslation,
24
23
  ResponsiveWrapper,
25
24
  showSnackbar,
26
25
  useLayoutType,
27
26
  useSession,
27
+ type Workspace2DefinitionProps,
28
+ Workspace2,
28
29
  } from '@openmrs/esm-framework';
29
- import { type BedPostPayload, type InitialData } from '../../types';
30
+ import { type BedFormWorkspaceConfig, type BedPostPayload } from '../../types';
30
31
  import { useBedTags, useLocationsWithAdmissionTag } from '../../summary/summary.resource';
31
32
  import { editBed, saveBed, useBedType, useBedTagMappings } from './bed-form.resource';
32
33
  import styles from './bed-form.workspace.scss';
@@ -34,19 +35,9 @@ import styles from './bed-form.workspace.scss';
34
35
  const OCCUPANCY_STATUSES = ['AVAILABLE', 'OCCUPIED'] as const;
35
36
  type OccupancyStatus = (typeof OCCUPANCY_STATUSES)[number];
36
37
 
37
- type BedFormWorkspaceProps = DefaultWorkspaceProps & {
38
- bed?: InitialData;
39
- mutateBeds: () => void;
40
- defaultLocation?: { display: string; uuid: string };
41
- };
42
-
43
- const BedFormWorkspace: React.FC<BedFormWorkspaceProps> = ({
38
+ const BedFormWorkspace: React.FC<Workspace2DefinitionProps<BedFormWorkspaceConfig>> = ({
39
+ workspaceProps: { bed, mutateBeds, defaultLocation },
44
40
  closeWorkspace,
45
- closeWorkspaceWithSavedChanges,
46
- promptBeforeClosing,
47
- bed,
48
- mutateBeds,
49
- defaultLocation,
50
41
  }) => {
51
42
  const { t } = useTranslation();
52
43
  const isTablet = useLayoutType() === 'tablet';
@@ -159,10 +150,6 @@ const BedFormWorkspace: React.FC<BedFormWorkspaceProps> = ({
159
150
  }
160
151
  }, [bedTagMappings, getDefaultValues, isLoadingBedTags, isEditing, reset]);
161
152
 
162
- useEffect(() => {
163
- promptBeforeClosing(() => isDirty);
164
- }, [isDirty, promptBeforeClosing]);
165
-
166
153
  const createBedPayload = (data: BedFormType) => ({
167
154
  ...(isEditing && { uuid: bed.uuid }),
168
155
  bedNumber: data.bedNumber,
@@ -193,7 +180,7 @@ const BedFormWorkspace: React.FC<BedFormWorkspaceProps> = ({
193
180
  });
194
181
 
195
182
  mutateBeds();
196
- closeWorkspaceWithSavedChanges();
183
+ closeWorkspace({ discardUnsavedChanges: true });
197
184
  } catch (error: unknown) {
198
185
  const subtitle =
199
186
  error instanceof Error && error.message
@@ -210,211 +197,215 @@ const BedFormWorkspace: React.FC<BedFormWorkspaceProps> = ({
210
197
  }
211
198
  };
212
199
 
200
+ const title = isEditing ? t('editBed', 'Edit bed') : t('addBed', 'Add bed');
201
+
213
202
  return (
214
- <Form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
215
- <div className={styles.formContainer}>
216
- <Stack gap={5}>
217
- <ResponsiveWrapper>
218
- <Controller
219
- control={control}
220
- name="bedNumber"
221
- render={({ field }) => (
222
- <TextInput
223
- helperText={t('bedNumberMaxCharsHelper', 'Maximum 10 characters')}
224
- id="bedNumber"
225
- invalid={!!errors.bedNumber?.message}
226
- invalidText={errors.bedNumber?.message}
227
- labelText={t('bedNumber', 'Bed number')}
228
- onChange={field.onChange}
229
- placeholder={t('bedNumberPlaceholder', 'e.g. CHA-201')}
230
- value={field.value}
231
- />
232
- )}
233
- />
234
- </ResponsiveWrapper>
235
- <div className={styles.rowContainer}>
203
+ <Workspace2 title={title} hasUnsavedChanges={isDirty}>
204
+ <Form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
205
+ <div className={styles.formContainer}>
206
+ <Stack gap={5}>
236
207
  <ResponsiveWrapper>
237
208
  <Controller
238
209
  control={control}
239
- name="bedRow"
210
+ name="bedNumber"
240
211
  render={({ field }) => (
241
- <NumberInput
242
- hideSteppers
243
- id="bedRow"
244
- invalid={!!errors.bedRow?.message}
245
- invalidText={errors.bedRow?.message}
246
- label={t('bedRow', 'Bed row')}
247
- onChange={(e, { value }) => field.onChange(value.toString())}
212
+ <TextInput
213
+ helperText={t('bedNumberMaxCharsHelper', 'Maximum 10 characters')}
214
+ id="bedNumber"
215
+ invalid={!!errors.bedNumber?.message}
216
+ invalidText={errors.bedNumber?.message}
217
+ labelText={t('bedNumber', 'Bed number')}
218
+ onChange={field.onChange}
219
+ placeholder={t('bedNumberPlaceholder', 'e.g. CHA-201')}
248
220
  value={field.value}
249
221
  />
250
222
  )}
251
223
  />
252
224
  </ResponsiveWrapper>
253
- </div>
254
- <div className={styles.rowContainer}>
225
+ <div className={styles.rowContainer}>
226
+ <ResponsiveWrapper>
227
+ <Controller
228
+ control={control}
229
+ name="bedRow"
230
+ render={({ field }) => (
231
+ <NumberInput
232
+ hideSteppers
233
+ id="bedRow"
234
+ invalid={!!errors.bedRow?.message}
235
+ invalidText={errors.bedRow?.message}
236
+ label={t('bedRow', 'Bed row')}
237
+ onChange={(e, { value }) => field.onChange(value.toString())}
238
+ value={field.value}
239
+ />
240
+ )}
241
+ />
242
+ </ResponsiveWrapper>
243
+ </div>
244
+ <div className={styles.rowContainer}>
245
+ <ResponsiveWrapper>
246
+ <Controller
247
+ control={control}
248
+ name="bedColumn"
249
+ render={({ field }) => (
250
+ <NumberInput
251
+ hideSteppers
252
+ id="bedColumn"
253
+ invalid={!!errors.bedColumn?.message}
254
+ invalidText={errors.bedColumn?.message}
255
+ label={t('bedColumn', 'Bed column')}
256
+ onChange={(e, { value }) => field.onChange(value.toString())}
257
+ value={field.value}
258
+ />
259
+ )}
260
+ />
261
+ </ResponsiveWrapper>
262
+ </div>
263
+
264
+ <ResponsiveWrapper>
265
+ <div className={styles.locationFieldContainer}>
266
+ <Controller
267
+ control={control}
268
+ name="location"
269
+ render={({ field: { onChange, onBlur, value, ref } }) => (
270
+ <ComboBox
271
+ disabled={!hasLocations}
272
+ id="location"
273
+ invalid={!!errors.location?.message}
274
+ invalidText={errors.location?.message}
275
+ items={allLocations}
276
+ itemToString={(location) => location?.display ?? ''}
277
+ onBlur={onBlur}
278
+ onChange={({ selectedItem }) => onChange(selectedItem)}
279
+ placeholder={
280
+ hasLocations
281
+ ? t('selectLocation', 'Select a location')
282
+ : t('noLocationsAvailable', 'No locations available')
283
+ }
284
+ ref={ref}
285
+ selectedItem={value}
286
+ titleText={t('location', 'Location')}
287
+ />
288
+ )}
289
+ />
290
+ </div>
291
+ </ResponsiveWrapper>
255
292
  <ResponsiveWrapper>
256
293
  <Controller
257
294
  control={control}
258
- name="bedColumn"
295
+ name="occupancyStatus"
259
296
  render={({ field }) => (
260
- <NumberInput
261
- hideSteppers
262
- id="bedColumn"
263
- invalid={!!errors.bedColumn?.message}
264
- invalidText={errors.bedColumn?.message}
265
- label={t('bedColumn', 'Bed column')}
266
- onChange={(e, { value }) => field.onChange(value.toString())}
267
- value={field.value}
268
- />
297
+ <Select
298
+ id="occupancyStatus"
299
+ invalid={!!errors.occupancyStatus?.message}
300
+ invalidText={errors.occupancyStatus?.message}
301
+ labelText={t('occupancyStatus', 'Occupancy status')}
302
+ onChange={field.onChange}
303
+ value={field.value}>
304
+ <SelectItem text={t('selectOccupancyStatus', 'Select occupancy status')} value="" />
305
+ {occupancyStatuses.map((status, index) => (
306
+ <SelectItem
307
+ key={`occupancy-${index}`}
308
+ text={t(status.toLowerCase(), status)}
309
+ value={status.toUpperCase()}
310
+ />
311
+ ))}
312
+ </Select>
269
313
  )}
270
314
  />
271
315
  </ResponsiveWrapper>
272
- </div>
273
316
 
274
- <ResponsiveWrapper>
275
- <div className={styles.locationFieldContainer}>
317
+ <ResponsiveWrapper>
276
318
  <Controller
277
319
  control={control}
278
- name="location"
279
- render={({ field: { onChange, onBlur, value, ref } }) => (
280
- <ComboBox
281
- disabled={!hasLocations}
282
- id="location"
283
- invalid={!!errors.location?.message}
284
- invalidText={errors.location?.message}
285
- items={allLocations}
286
- itemToString={(location) => location?.display ?? ''}
287
- onBlur={onBlur}
288
- onChange={({ selectedItem }) => onChange(selectedItem)}
289
- placeholder={
290
- hasLocations
291
- ? t('selectLocation', 'Select a location')
292
- : t('noLocationsAvailable', 'No locations available')
293
- }
294
- ref={ref}
295
- selectedItem={value}
296
- titleText={t('location', 'Location')}
297
- />
320
+ name="bedType"
321
+ render={({ field }) => (
322
+ <Select
323
+ disabled={!availableBedTypes.length}
324
+ id="bedType"
325
+ invalid={!!errors.bedType?.message}
326
+ invalidText={errors.bedType?.message}
327
+ labelText={t('bedType', 'Bed Type')}
328
+ onChange={field.onChange}
329
+ value={field.value}>
330
+ <SelectItem text={t('selectBedType', 'Select bed type')} value="" />
331
+ {availableBedTypes.map((bedType, index) => (
332
+ <SelectItem key={`bedType-${index}`} text={bedType.name} value={bedType.name} />
333
+ ))}
334
+ </Select>
298
335
  )}
299
336
  />
300
- </div>
301
- </ResponsiveWrapper>
302
- <ResponsiveWrapper>
303
- <Controller
304
- control={control}
305
- name="occupancyStatus"
306
- render={({ field }) => (
307
- <Select
308
- id="occupancyStatus"
309
- invalid={!!errors.occupancyStatus?.message}
310
- invalidText={errors.occupancyStatus?.message}
311
- labelText={t('occupancyStatus', 'Occupancy status')}
312
- onChange={field.onChange}
313
- value={field.value}>
314
- <SelectItem text={t('selectOccupancyStatus', 'Select occupancy status')} value="" />
315
- {occupancyStatuses.map((status, index) => (
316
- <SelectItem
317
- key={`occupancy-${index}`}
318
- text={t(status.toLowerCase(), status)}
319
- value={status.toUpperCase()}
320
- />
321
- ))}
322
- </Select>
323
- )}
324
- />
325
- </ResponsiveWrapper>
326
-
327
- <ResponsiveWrapper>
328
- <Controller
329
- control={control}
330
- name="bedType"
331
- render={({ field }) => (
332
- <Select
333
- disabled={!availableBedTypes.length}
334
- id="bedType"
335
- invalid={!!errors.bedType?.message}
336
- invalidText={errors.bedType?.message}
337
- labelText={t('bedType', 'Bed Type')}
338
- onChange={field.onChange}
339
- value={field.value}>
340
- <SelectItem text={t('selectBedType', 'Select bed type')} value="" />
341
- {availableBedTypes.map((bedType, index) => (
342
- <SelectItem key={`bedType-${index}`} text={bedType.name} value={bedType.name} />
343
- ))}
344
- </Select>
345
- )}
346
- />
347
- </ResponsiveWrapper>
337
+ </ResponsiveWrapper>
348
338
 
349
- <ResponsiveWrapper>
350
- <Controller
351
- control={control}
352
- name="bedTags"
353
- render={({ field: { onChange, value } }) => {
354
- const selectedItems = (value || [])
355
- .map((tag) => {
356
- const fullTag = availableBedTags.find((t) => t.uuid === tag.uuid || t.name === tag.name);
357
- return fullTag || tag;
358
- })
359
- .filter(Boolean);
339
+ <ResponsiveWrapper>
340
+ <Controller
341
+ control={control}
342
+ name="bedTags"
343
+ render={({ field: { onChange, value } }) => {
344
+ const selectedItems = (value || [])
345
+ .map((tag) => {
346
+ const fullTag = availableBedTags.find((t) => t.uuid === tag.uuid || t.name === tag.name);
347
+ return fullTag || tag;
348
+ })
349
+ .filter(Boolean);
360
350
 
361
- return (
362
- <div>
363
- <MultiSelect
364
- disabled={!availableBedTags.length}
365
- id="bedTags"
366
- invalid={!!errors.bedTags?.message}
367
- invalidText={errors.bedTags?.message}
368
- items={availableBedTags}
369
- itemToString={(item) => item?.name ?? ''}
370
- label={t('selectBedTags', 'Select bed tags')}
371
- onChange={({ selectedItems }) => onChange(selectedItems)}
372
- selectedItems={selectedItems}
373
- titleText={t('bedTags', 'Bed Tags')}
374
- />
375
- {selectedItems && selectedItems.length > 0 && (
376
- <div className={styles.tagContainer}>
377
- {selectedItems.map((tag, index) => (
378
- <Tag
379
- key={tag.uuid || tag.id || index}
380
- type="blue"
381
- onClose={() => {
382
- const updatedTags = selectedItems.filter((_, i) => i !== index);
383
- onChange(updatedTags);
384
- }}>
385
- {tag.name}
386
- </Tag>
387
- ))}
388
- </div>
389
- )}
390
- </div>
391
- );
392
- }}
393
- />
394
- </ResponsiveWrapper>
395
- </Stack>
396
- </div>
397
- <ButtonSet
398
- className={classNames({
399
- [styles.tablet]: isTablet,
400
- [styles.desktop]: !isTablet,
401
- })}>
402
- <Button className={styles.buttonContainer} kind="secondary" onClick={() => closeWorkspace()}>
403
- {getCoreTranslation('cancel')}
404
- </Button>
405
- <Button
406
- className={styles.button}
407
- disabled={isSubmitting || !isDirty || !hasLocations}
408
- kind="primary"
409
- type="submit">
410
- {isSubmitting ? (
411
- <InlineLoading className={styles.spinner} description={t('saving', 'Saving') + '...'} />
412
- ) : (
413
- <span>{t('saveAndClose', 'Save & close')}</span>
414
- )}
415
- </Button>
416
- </ButtonSet>
417
- </Form>
351
+ return (
352
+ <div>
353
+ <MultiSelect
354
+ disabled={!availableBedTags.length}
355
+ id="bedTags"
356
+ invalid={!!errors.bedTags?.message}
357
+ invalidText={errors.bedTags?.message}
358
+ items={availableBedTags}
359
+ itemToString={(item) => item?.name ?? ''}
360
+ label={t('selectBedTags', 'Select bed tags')}
361
+ onChange={({ selectedItems }) => onChange(selectedItems)}
362
+ selectedItems={selectedItems}
363
+ titleText={t('bedTags', 'Bed Tags')}
364
+ />
365
+ {selectedItems && selectedItems.length > 0 && (
366
+ <div className={styles.tagContainer}>
367
+ {selectedItems.map((tag, index) => (
368
+ <Tag
369
+ key={tag.uuid || tag.id || index}
370
+ type="blue"
371
+ onClose={() => {
372
+ const updatedTags = selectedItems.filter((_, i) => i !== index);
373
+ onChange(updatedTags);
374
+ }}>
375
+ {tag.name}
376
+ </Tag>
377
+ ))}
378
+ </div>
379
+ )}
380
+ </div>
381
+ );
382
+ }}
383
+ />
384
+ </ResponsiveWrapper>
385
+ </Stack>
386
+ </div>
387
+ <ButtonSet
388
+ className={classNames({
389
+ [styles.tablet]: isTablet,
390
+ [styles.desktop]: !isTablet,
391
+ })}>
392
+ <Button className={styles.buttonContainer} kind="secondary" onClick={() => closeWorkspace()}>
393
+ {getCoreTranslation('cancel')}
394
+ </Button>
395
+ <Button
396
+ className={styles.button}
397
+ disabled={isSubmitting || !isDirty || !hasLocations}
398
+ kind="primary"
399
+ type="submit">
400
+ {isSubmitting ? (
401
+ <InlineLoading className={styles.spinner} description={t('saving', 'Saving') + '...'} />
402
+ ) : (
403
+ <span>{t('saveAndClose', 'Save & close')}</span>
404
+ )}
405
+ </Button>
406
+ </ButtonSet>
407
+ </Form>
408
+ </Workspace2>
418
409
  );
419
410
  };
420
411
 
package/src/routes.json CHANGED
@@ -64,12 +64,25 @@
64
64
  "component": "newBedTypeModal"
65
65
  }
66
66
  ],
67
- "workspaces": [
67
+ "workspaceGroups2": [
68
68
  {
69
- "name": "bed-form-workspace",
70
- "title": "Add or edit bed",
71
- "component": "bedFormWorkspace",
72
- "type": "workspace"
69
+ "name": "bed-management-group",
70
+ "persistence": "closable"
71
+ }
72
+ ],
73
+ "workspaceWindows2": [
74
+ {
75
+ "name": "bed-management-window",
76
+ "group": "bed-management-group",
77
+ "canMaximize": true,
78
+ "width": "narrow"
79
+ }
80
+ ],
81
+ "workspaces2": [
82
+ {
83
+ "name": "bed-form-workspace",
84
+ "component": "bedFormWorkspace",
85
+ "window": "bed-management-window"
73
86
  }
74
87
  ]
75
88
  }
package/src/types.ts CHANGED
@@ -202,3 +202,33 @@ export interface BedDetails extends Bed {
202
202
  }
203
203
 
204
204
  export type WorkspaceMode = 'add' | 'edit';
205
+
206
+ export interface CloseWorkspaceOptions {
207
+ ignoreChanges?: boolean;
208
+ onWorkspaceClose?: () => void;
209
+ }
210
+
211
+ export interface BedWorkspaceData {
212
+ uuid: string;
213
+ bedNumber: string;
214
+ status: string;
215
+ row: number;
216
+ column: number;
217
+ bedType?: {
218
+ name: string;
219
+ };
220
+ location?: {
221
+ display: string;
222
+ uuid: string;
223
+ };
224
+ bedTags?: Array<{
225
+ uuid: string;
226
+ name: string;
227
+ }>;
228
+ }
229
+
230
+ export interface BedFormWorkspaceConfig {
231
+ bed?: BedWorkspaceData;
232
+ mutateBeds: () => void;
233
+ defaultLocation?: { display: string; uuid: string };
234
+ }
@@ -17,7 +17,7 @@ import {
17
17
  Tag,
18
18
  } from '@carbon/react';
19
19
  import { ArrowLeft, Add } from '@carbon/react/icons';
20
- import { launchWorkspace, navigate, usePagination } from '@openmrs/esm-framework';
20
+ import { launchWorkspace2, navigate, usePagination } from '@openmrs/esm-framework';
21
21
  import { useBedsForLocation, useLocationName } from '../summary/summary.resource';
22
22
  import { type Bed, type WorkspaceMode } from '../types';
23
23
  import Header from '../header/header.component';
@@ -85,8 +85,7 @@ const WardWithBeds: React.FC = () => {
85
85
  }, [paginatedData]);
86
86
 
87
87
  const handleLaunchBedFormWorkspace = (mode: WorkspaceMode, bed?: Bed) => {
88
- launchWorkspace('bed-form-workspace', {
89
- workspaceTitle: mode === 'add' ? t('addBed', 'Add bed') : t('editBed', 'Edit bed'),
88
+ launchWorkspace2('bed-form-workspace', {
90
89
  mutateBeds: mutate,
91
90
  defaultLocation: { display: name, uuid: location },
92
91
  ...(mode === 'edit' && bed ? { bed } : {}),