@mbehenri/openmrs-esm-opentms-meet-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 (81) 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 +14 -0
  7. package/.turbo.json +18 -0
  8. package/.yarn/install-state.gz +0 -0
  9. package/.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs +541 -0
  10. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
  11. package/.yarn/releases/yarn-3.6.1.cjs +874 -0
  12. package/.yarnrc.yml +9 -0
  13. package/LICENSE +401 -0
  14. package/README.md +26 -0
  15. package/__mocks__/react-i18next.js +55 -0
  16. package/e2e/README.md +115 -0
  17. package/e2e/core/global-setup.ts +32 -0
  18. package/e2e/core/index.ts +1 -0
  19. package/e2e/core/test.ts +20 -0
  20. package/e2e/fixtures/api.ts +26 -0
  21. package/e2e/fixtures/index.ts +1 -0
  22. package/e2e/pages/home-page.ts +9 -0
  23. package/e2e/pages/index.ts +1 -0
  24. package/e2e/specs/sample-test.spec.ts +11 -0
  25. package/e2e/support/github/Dockerfile +34 -0
  26. package/e2e/support/github/docker-compose.yml +24 -0
  27. package/e2e/support/github/run-e2e-docker-env.sh +49 -0
  28. package/example.env +6 -0
  29. package/i18next-parser.config.js +89 -0
  30. package/jest.config.js +31 -0
  31. package/package.json +108 -0
  32. package/playwright.config.ts +32 -0
  33. package/src/Extensions/AppointmentTabExt.tsx +23 -0
  34. package/src/Extensions/DemandTabExt.tsx +14 -0
  35. package/src/Extensions/MeetIframeExt.tsx +14 -0
  36. package/src/Extensions/ValidateDemandFormExt.tsx +14 -0
  37. package/src/assets/img/Logo-texte.png +0 -0
  38. package/src/assets/img/Logo-texte.sim.white.png +0 -0
  39. package/src/assets/img/Logo-texte.white.png +0 -0
  40. package/src/assets/img/Logo.png +0 -0
  41. package/src/assets/img/favicon.ico +0 -0
  42. package/src/components/Appointment/index.scss +91 -0
  43. package/src/components/Appointment/index.tsx +207 -0
  44. package/src/components/Appointment/menu.scss +7 -0
  45. package/src/components/Appointment/menu.tsx +48 -0
  46. package/src/components/Appointment/tab.tsx +162 -0
  47. package/src/components/Demand/form.scss +19 -0
  48. package/src/components/Demand/form.tsx +236 -0
  49. package/src/components/Demand/index.tsx +0 -0
  50. package/src/components/Demand/tab.scss +145 -0
  51. package/src/components/Demand/tab.tsx +315 -0
  52. package/src/components/EmptyLayout/index.scss +69 -0
  53. package/src/components/EmptyLayout/index.tsx +32 -0
  54. package/src/components/MeetIframe/index.scss +56 -0
  55. package/src/components/MeetIframe/index.tsx +117 -0
  56. package/src/config-schema.ts +45 -0
  57. package/src/dashboard.meta.ts +12 -0
  58. package/src/declarations.d.ts +6 -0
  59. package/src/index.ts +75 -0
  60. package/src/pages/home/home.component.tsx +8 -0
  61. package/src/privileges/doctor.ts +213 -0
  62. package/src/repositories/Opencare/index.ts +12 -0
  63. package/src/repositories/Opencare/prodRepository.ts +176 -0
  64. package/src/repositories/Opencare/repository.ts +34 -0
  65. package/src/repositories/TypeRepository.ts +1 -0
  66. package/src/repositories/env.ts +7 -0
  67. package/src/repositories/errors.ts +13 -0
  68. package/src/root.component.tsx +46 -0
  69. package/src/root.scss +15 -0
  70. package/src/root.test.tsx +54 -0
  71. package/src/routes.json +44 -0
  72. package/src/services/doctor.ts +165 -0
  73. package/src/setup-tests.ts +1 -0
  74. package/src/utils.ts +41 -0
  75. package/translations/en.json +1 -0
  76. package/translations/es.json +24 -0
  77. package/translations/fr.json +24 -0
  78. package/translations/he.json +24 -0
  79. package/translations/km.json +24 -0
  80. package/tsconfig.json +24 -0
  81. package/webpack.config.js +1 -0
@@ -0,0 +1,207 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useTranslation } from "react-i18next";
3
+ import {
4
+ ContentSwitcher,
5
+ DataTableSkeleton,
6
+ Layer,
7
+ Switch,
8
+ Tile,
9
+ } from "@carbon/react";
10
+ import {
11
+ getCurrentUser,
12
+ useConfig,
13
+ useLayoutType,
14
+ useSession,
15
+ //launchWorkspaces
16
+ } from "@openmrs/esm-framework";
17
+ import PatientAppointmentsTable from "./tab";
18
+ import styles from "./index.scss";
19
+ import DoctorService, { AppointmentTypes } from "../../services/doctor";
20
+ import env from "../../repositories/env";
21
+ import { MeetIframe } from "../MeetIframe";
22
+
23
+ interface PatientAppointmentsBaseProps {
24
+ patientUuid: string;
25
+ }
26
+
27
+ const PatientAppointmentsBase: React.FC<PatientAppointmentsBaseProps> = ({
28
+ patientUuid,
29
+ }) => {
30
+ //I. hooks
31
+ const { t } = useTranslation();
32
+ const isTablet = useLayoutType() === "tablet";
33
+ const [contentSwitcherValue, setContentSwitcherValue] = useState(
34
+ AppointmentTypes.UPCOMING
35
+ );
36
+ const [loading, setLoading] = useState(true);
37
+ const [error, setError] = useState(false);
38
+ const [appointments, setAppointments] = useState<Map<number, Array<any>>>(
39
+ new Map()
40
+ );
41
+
42
+ const [tokenNextcloud, setTokenNextcloud] = useState("");
43
+
44
+ const { user } = useSession();
45
+
46
+ const [url, setUrl] = useState("");
47
+
48
+ //recupération de la configuration
49
+ const conf = useConfig();
50
+
51
+ // update env variable
52
+
53
+ env.API_HOST = conf["API_HOST"];
54
+ env.API_PASSWORD = conf["API_PASSWORD"];
55
+ env.API_PORT = conf["API_PORT"];
56
+ env.API_USER = conf["API_USER"];
57
+ env.API_SECURE = conf["API_SECURE"];
58
+ const doctorService = useMemo(() => DoctorService.getInstance(), []);
59
+
60
+ // chargement des appointements
61
+ useEffect(() => {
62
+ const fun = async () => {
63
+ setLoading(true);
64
+ setError(false);
65
+ await doctorService
66
+ .getAppointments(patientUuid, user.person.uuid)
67
+ .then((appointments) => {
68
+ if (appointments) {
69
+ setAppointments(appointments);
70
+ } else {
71
+ setError(true);
72
+ }
73
+ })
74
+ .finally(() => {
75
+ setLoading(false);
76
+ });
77
+ };
78
+ fun();
79
+
80
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
81
+ return () => {};
82
+ }, [doctorService, patientUuid, user.person.uuid]);
83
+
84
+ useEffect(() => {
85
+ const fun = async () => {
86
+ setLoading(true);
87
+ setError(false);
88
+ await doctorService
89
+ .getTokenNextcloud(patientUuid)
90
+ .then((token) => {
91
+ setTokenNextcloud(token);
92
+ })
93
+ .catch((e) => console.error(e));
94
+ };
95
+ fun();
96
+
97
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
98
+ return () => {};
99
+ }, [doctorService, patientUuid]);
100
+
101
+ const handleUrlMeeting = useCallback((url: string) => {
102
+ setUrl(url);
103
+ /* launchWorkspace("opencare-meet-iframe", {
104
+ url,
105
+ context: "view",
106
+ }); */
107
+ }, []);
108
+
109
+ // II. returns
110
+ if (loading) {
111
+ return <DataTableSkeleton role="progressbar" compact={!isTablet} zebra />;
112
+ }
113
+
114
+ if (error) {
115
+ return <span>Error</span>;
116
+ }
117
+
118
+ return (
119
+ <>
120
+ <div className={styles.contentSwitcherWrapper}>
121
+ <ContentSwitcher
122
+ size={isTablet ? "md" : "sm"}
123
+ onChange={({ index }) => {
124
+ setContentSwitcherValue(index);
125
+ }}
126
+ >
127
+ <Switch name={"upcoming"} text={t("upcoming", "Upcoming")} />
128
+ <Switch name={"today"} text={t("today", "Today")} />
129
+ <Switch name={"past"} text={t("past", "Past")} />
130
+ </ContentSwitcher>
131
+ </div>
132
+
133
+ {(() => {
134
+ if (appointments.get(contentSwitcherValue).length > 0) {
135
+ return (
136
+ <PatientAppointmentsTable
137
+ appointments={appointments.get(contentSwitcherValue)}
138
+ handleUrlMeeting={handleUrlMeeting}
139
+ />
140
+ );
141
+ }
142
+ if (contentSwitcherValue === AppointmentTypes.UPCOMING) {
143
+ return (
144
+ <Layer>
145
+ <Tile className={styles.tile}>
146
+ <p className={styles.content}>
147
+ {t(
148
+ "noUpcomingAppointmentsForPatient",
149
+ "There are no upcoming appointments to display for this patient"
150
+ )}
151
+ </p>
152
+ </Tile>
153
+ </Layer>
154
+ );
155
+ }
156
+ if (contentSwitcherValue === AppointmentTypes.TODAY) {
157
+ return (
158
+ <Layer>
159
+ <Tile className={styles.tile}>
160
+ <p className={styles.content}>
161
+ {
162
+ "There are no appointments scheduled for today to display for this patient"
163
+ }
164
+ </p>
165
+ </Tile>
166
+ </Layer>
167
+ );
168
+ }
169
+ if (contentSwitcherValue === AppointmentTypes.PAST) {
170
+ return (
171
+ <Layer>
172
+ <Tile className={styles.tile}>
173
+ <p className={styles.content}>
174
+ {"There are no past appointments to display for this patient"}
175
+ </p>
176
+ </Tile>
177
+ </Layer>
178
+ );
179
+ }
180
+
181
+ if (url != "") {
182
+ return (
183
+ <iframe
184
+ title="Web Meeting"
185
+ src={url}
186
+ style={{ width: "100%", height: "500px" }}
187
+ />
188
+ );
189
+ }
190
+ })()}
191
+
192
+ {(() => {
193
+ if (url != "") {
194
+ return (
195
+ <MeetIframe
196
+ url={url}
197
+ username={user.display}
198
+ token={tokenNextcloud}
199
+ />
200
+ );
201
+ }
202
+ })()}
203
+ </>
204
+ );
205
+ };
206
+
207
+ export default PatientAppointmentsBase;
@@ -0,0 +1,7 @@
1
+ .layer {
2
+ height: 100%;
3
+ }
4
+
5
+ .menuItem {
6
+ max-width: none;
7
+ }
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+ import { useTranslation } from "react-i18next";
3
+
4
+ import { Layer, OverflowMenu, OverflowMenuItem } from "@carbon/react";
5
+ import { useLayoutType } from "@openmrs/esm-framework";
6
+ import styles from "./menu.scss";
7
+
8
+ interface appointmentsActionMenuProps {
9
+ appointment: any;
10
+ patientUuid: string;
11
+ }
12
+
13
+ export const PatientAppointmentsActionMenu = ({
14
+ appointment,
15
+ patientUuid,
16
+ }: appointmentsActionMenuProps) => {
17
+ const { t } = useTranslation();
18
+ const isTablet = useLayoutType() === "tablet";
19
+
20
+ return (
21
+ <Layer className={styles.layer}>
22
+ <OverflowMenu
23
+ aria-label="Edit or delete appointment"
24
+ size={isTablet ? "lg" : "sm"}
25
+ flipped
26
+ align="left"
27
+ >
28
+ <OverflowMenuItem
29
+ className={styles.menuItem}
30
+ id="editAppointment"
31
+ itemText={t("edit", "Edit")}
32
+ />
33
+ <OverflowMenuItem
34
+ className={styles.menuItem}
35
+ id="roomAppointment"
36
+ itemText={t("room", "Go")}
37
+ />
38
+ <OverflowMenuItem
39
+ className={styles.menuItem}
40
+ id="cancelAppointment"
41
+ itemText={t("cancel", "Cancel")}
42
+ isDelete={true}
43
+ hasDivider
44
+ />
45
+ </OverflowMenu>
46
+ </Layer>
47
+ );
48
+ };
@@ -0,0 +1,162 @@
1
+ import React, { useCallback, useMemo, useState } from "react";
2
+ import { useTranslation } from "react-i18next";
3
+ import {
4
+ DataTable,
5
+ type DataTableHeader,
6
+ Table,
7
+ Layer,
8
+ TableCell,
9
+ TableContainer,
10
+ TableBody,
11
+ TableHead,
12
+ TableHeader,
13
+ TableRow,
14
+ Pagination,
15
+ } from "@carbon/react";
16
+ import {
17
+ formatDatetime,
18
+ isDesktop,
19
+ parseDate,
20
+ useLayoutType,
21
+ usePagination,
22
+ } from "@openmrs/esm-framework";
23
+ import { Button } from "@carbon/react";
24
+ import { getPageSizes } from "../../utils";
25
+
26
+ interface AppointmentTableProps {
27
+ appointments: Array<any>;
28
+ handleUrlMeeting: (url: string) => void;
29
+ }
30
+
31
+ const PatientAppointmentsTable: React.FC<AppointmentTableProps> = ({
32
+ appointments,
33
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
34
+ handleUrlMeeting = (url) => {},
35
+ }) => {
36
+ // I. hooks
37
+
38
+ const { t } = useTranslation();
39
+ const layout = useLayoutType();
40
+ const responsiveSize = isDesktop(layout) ? "sm" : "lg";
41
+ const [pageSize, setPageSize] = useState(5);
42
+
43
+ const tableRows = useMemo(
44
+ () =>
45
+ appointments.map((appointment) => {
46
+ return {
47
+ id: appointment.uuid,
48
+ date: formatDatetime(parseDate(appointment.startDateTime), {
49
+ mode: "wide",
50
+ }),
51
+ location: appointment?.location ? appointment?.location : "——",
52
+ service: appointment.service,
53
+ status: appointment.status,
54
+ type: appointment.appointmentKind
55
+ ? appointment.appointmentKind
56
+ : "——",
57
+ notes: appointment.comments ? appointment.comments : "——",
58
+ };
59
+ }),
60
+ [appointments]
61
+ );
62
+
63
+ const mapAppointments = useMemo(() => {
64
+ const map = new Map();
65
+ appointments.forEach((appointment) => {
66
+ map.set(appointment.uuid, appointment);
67
+ });
68
+ return map;
69
+ }, [appointments]);
70
+
71
+ const { results, goTo, currentPage } = usePagination(tableRows, pageSize);
72
+
73
+ const tableHeaders: Array<typeof DataTableHeader> = useMemo(
74
+ () => [
75
+ { key: "date", header: t("date", "Date") },
76
+ { key: "location", header: t("location", "Location") },
77
+ { key: "service", header: t("service", "Service") },
78
+ { key: "status", header: t("status", "Status") },
79
+ { key: "type", header: t("type", "Type") },
80
+ { key: "notes", header: t("notes", "Notes") },
81
+ ],
82
+ [t]
83
+ );
84
+
85
+ const HandleJoin = useCallback(
86
+ (appointment) => {
87
+ handleUrlMeeting(appointment.linkRoom);
88
+ },
89
+ [handleUrlMeeting]
90
+ );
91
+
92
+ return (
93
+ <Layer style={{ marginBottom: "1rem" }}>
94
+ <DataTable
95
+ rows={results}
96
+ headers={tableHeaders}
97
+ isSortable
98
+ size={responsiveSize}
99
+ useZebraStyles
100
+ >
101
+ {({ rows, headers, getHeaderProps, getTableProps }) => (
102
+ <TableContainer>
103
+ <Table {...getTableProps()}>
104
+ <TableHead>
105
+ <TableRow>
106
+ {headers.map((header) => (
107
+ <TableHeader
108
+ {...getHeaderProps({
109
+ header,
110
+ isSortable: header.isSortable,
111
+ })}
112
+ >
113
+ {header.header?.content ?? header.header}
114
+ </TableHeader>
115
+ ))}
116
+ <TableHeader />
117
+ </TableRow>
118
+ </TableHead>
119
+ <TableBody>
120
+ {rows.map((row, i) => (
121
+ <TableRow key={row.id}>
122
+ {row.cells.map((cell) => (
123
+ <TableCell key={cell.id}>
124
+ {cell.value?.content ?? cell.value}
125
+ </TableCell>
126
+ ))}
127
+ <TableCell className="cds--table-column-menu">
128
+ {/* <PatientAppointmentsActionMenu
129
+ appointment={appointments[i]}
130
+ patientUuid={patientUuid}
131
+ /> */}
132
+ <Button
133
+ size="small"
134
+ onClick={() => {
135
+ HandleJoin(mapAppointments.get(row.id));
136
+ }}
137
+ >
138
+ Joindre
139
+ </Button>
140
+ </TableCell>
141
+ </TableRow>
142
+ ))}
143
+ </TableBody>
144
+ </Table>
145
+ </TableContainer>
146
+ )}
147
+ </DataTable>
148
+ <Pagination
149
+ page={currentPage}
150
+ pageSize={pageSize}
151
+ pageSizes={getPageSizes(results, 5) ?? []}
152
+ onChange={({ page, pageSize }) => {
153
+ goTo(page);
154
+ setPageSize(pageSize);
155
+ }}
156
+ totalItems={results.length}
157
+ />
158
+ </Layer>
159
+ );
160
+ };
161
+
162
+ export default PatientAppointmentsTable;
@@ -0,0 +1,19 @@
1
+ .formGroupsContainer {
2
+ margin: 1rem
3
+ }
4
+
5
+ .formGroupContainer {
6
+ margin-top: 0.5rem;
7
+ }
8
+
9
+ .buttonContainer {
10
+ display: flex;
11
+ width: 100%;
12
+ background-color: #0d8888;
13
+ }
14
+
15
+ .formButton {
16
+ max-width: unset !important;
17
+ flex: 1;
18
+ height: 4rem;
19
+ }
@@ -0,0 +1,236 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+ import {
3
+ Form,
4
+ FormGroup,
5
+ Dropdown,
6
+ DatePicker,
7
+ DatePickerInput,
8
+ NumberInput,
9
+ TimePicker,
10
+ TimePickerSelect,
11
+ SelectItem,
12
+ Button,
13
+ } from "@carbon/react";
14
+ import { Layer, Tile } from "@carbon/react";
15
+ import { showToast, useConfig /*useLayoutType*/ } from "@openmrs/esm-framework";
16
+ import styles from "./form.scss";
17
+ import env from "../../repositories/env";
18
+ import DoctorService from "../../services/doctor";
19
+ //import { inLocalTimeOffsetUTC } from "../../utils";
20
+
21
+ export interface ValidateDemandFormProps {
22
+ demand: any;
23
+ onClose: () => void;
24
+ }
25
+
26
+ interface Doctor {
27
+ id: string;
28
+ name: string;
29
+ }
30
+
31
+ export const ValidateDemandForm: React.FC<ValidateDemandFormProps> = ({
32
+ demand,
33
+ onClose,
34
+ }) => {
35
+ //const isTablet = useLayoutType() === "tablet";
36
+
37
+ const [selectedDoctor, setSelectedDoctor] = useState<Doctor | null>(null);
38
+ const [startDate, setStartDate] = useState<Date | null>(null);
39
+ const [startTime, setStartTime] = useState<string>("00:00");
40
+ const [ampm, setAmpm] = useState<string>("AM");
41
+ const [duration, setDuration] = useState<number>(30);
42
+ const [doctors, setDoctors] = useState([]);
43
+ //recupération de la configuration
44
+
45
+ const [processing, setProcessing] = useState(false);
46
+ const conf = useConfig();
47
+
48
+ // update env variable
49
+
50
+ env.API_HOST = conf["API_HOST"];
51
+ env.API_PASSWORD = conf["API_PASSWORD"];
52
+ env.API_PORT = conf["API_PORT"];
53
+ env.API_USER = conf["API_USER"];
54
+ env.API_SECURE = conf["API_SECURE"];
55
+ const doctorService = useMemo(() => DoctorService.getInstance(), []);
56
+
57
+ useEffect(() => {
58
+ const fun = async () => {
59
+ await doctorService.getProviders().then((doctors) => {
60
+ if (doctors) {
61
+ setDoctors(doctors);
62
+ }
63
+ });
64
+ };
65
+ fun();
66
+
67
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
68
+ return () => {};
69
+ }, [doctorService]);
70
+
71
+ const handleDoctorChange = useCallback(
72
+ (selectedItem: any) => {
73
+ const doctor = doctors.find(
74
+ (doc) => doc.name === selectedItem.selectedItem
75
+ );
76
+ setSelectedDoctor(doctor || null);
77
+ },
78
+ [doctors]
79
+ );
80
+
81
+ const handleStartDateChange = useCallback((date: Date[]) => {
82
+ setStartDate(date[0] || null);
83
+ }, []);
84
+
85
+ const handleStartTimeChange = useCallback((event: any) => {
86
+ setStartTime(event.target.value);
87
+ }, []);
88
+
89
+ const handleAmpmChange = useCallback((event: any) => {
90
+ setAmpm(event.target.value);
91
+ }, []);
92
+
93
+ const handleDurationChange = useCallback((event: any) => {
94
+ setDuration(event.target.value);
95
+ }, []);
96
+
97
+ const handleSubmit = async () => {
98
+ if (startDate && startTime && duration > 0) {
99
+ if (!processing) {
100
+ setProcessing(true);
101
+ let [hours, minutes] = startTime.split(":").map(Number);
102
+ if (ampm === "PM" && hours < 12) {
103
+ hours += 12;
104
+ }
105
+ if (ampm === "AM" && hours === 12) {
106
+ hours = 0;
107
+ }
108
+ const combinedDateTime = new Date(startDate);
109
+ combinedDateTime.setHours(hours, minutes);
110
+
111
+ // Gestion de la soumission du formulaire
112
+ await doctorService
113
+ .validateDemand(
114
+ demand.id,
115
+ selectedDoctor.id,
116
+ combinedDateTime,
117
+ duration
118
+ )
119
+ .then((res) => {
120
+ /* const res = true; */
121
+ if (res) {
122
+ showToast({
123
+ description: `the demand initiated by ${demand.patient} have been validated`,
124
+ kind: "success",
125
+ });
126
+ } else {
127
+ showToast({
128
+ description: `error during validation`,
129
+ kind: "error",
130
+ });
131
+ }
132
+ })
133
+ .finally(() => {
134
+ setProcessing(false);
135
+ if (onClose) {
136
+ onClose();
137
+ }
138
+ });
139
+ }
140
+ }
141
+ };
142
+
143
+ const handleCancel = useCallback(() => {
144
+ // Gestion de l'annulation du formulaire
145
+ /* showToast({ description: `cancel` }); */
146
+ if (!processing) {
147
+ if (onClose) {
148
+ onClose();
149
+ }
150
+ }
151
+ }, [onClose, processing]);
152
+
153
+ return (
154
+ <Layer>
155
+ <Tile>
156
+ <h2>Validate demand</h2>
157
+ </Tile>
158
+ <Form>
159
+ <div className={styles.formGroupsContainer}>
160
+ <FormGroup
161
+ legendText="Provider"
162
+ className={styles.formGroupContainer}
163
+ >
164
+ <Dropdown
165
+ id="doctor-dropdown"
166
+ items={doctors.map((doc) => doc.name)}
167
+ onChange={handleDoctorChange}
168
+ />
169
+ </FormGroup>
170
+
171
+ <FormGroup
172
+ legendText="StartDateTime"
173
+ className={styles.formGroupContainer}
174
+ style={{ display: "flex" }}
175
+ >
176
+ <DatePicker
177
+ dateFormat="d/m/Y"
178
+ datePickerType="single"
179
+ onChange={handleStartDateChange}
180
+ >
181
+ <DatePickerInput
182
+ id="start-date-picker"
183
+ placeholder="jj/mm/aaaa"
184
+ />
185
+ </DatePicker>
186
+ <TimePicker
187
+ id="start-time-picker"
188
+ onChange={handleStartTimeChange}
189
+ value={startTime}
190
+ />
191
+ <TimePickerSelect
192
+ id="time-picker-select-ampm"
193
+ labelText="AM/PM"
194
+ onChange={handleAmpmChange}
195
+ >
196
+ <SelectItem value="AM" text="AM" />
197
+ <SelectItem value="PM" text="PM" />
198
+ </TimePickerSelect>
199
+ </FormGroup>
200
+
201
+ <FormGroup
202
+ legendText="Duration (minutes)"
203
+ className={styles.formGroupContainer}
204
+ >
205
+ <NumberInput
206
+ id="duration-input"
207
+ min={10}
208
+ value={duration}
209
+ onChange={handleDurationChange}
210
+ />
211
+ </FormGroup>
212
+ </div>
213
+ <div className={styles.buttonContainer}>
214
+ <Button
215
+ kind="secondary"
216
+ size="large"
217
+ className={styles.formButton}
218
+ onClick={handleCancel}
219
+ disable={processing}
220
+ >
221
+ Cancel
222
+ </Button>
223
+ <Button
224
+ kind="primary"
225
+ size="large"
226
+ className={styles.formButton}
227
+ disable={processing}
228
+ onClick={handleSubmit}
229
+ >
230
+ {processing ? "Validating ..." : "Validate"}
231
+ </Button>
232
+ </div>
233
+ </Form>
234
+ </Layer>
235
+ );
236
+ };
File without changes