@medplum/react 0.9.21 → 0.9.22

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.
@@ -1511,6 +1511,64 @@ div.medplum-nav-menu-container {
1511
1511
  margin: 0;
1512
1512
  }
1513
1513
 
1514
+ .medplum-calendar-table {
1515
+ width: 350px;
1516
+ }
1517
+
1518
+ .medplum-calendar-table th {
1519
+ font-weight: normal;
1520
+ font-size: 11px;
1521
+ padding: 8px;
1522
+ text-align: center;
1523
+ }
1524
+
1525
+ .medplum-calendar-table td {
1526
+ padding: 2px 4px;
1527
+ }
1528
+
1529
+ .medplum-calendar-table button {
1530
+ width: 44px;
1531
+ height: 44px;
1532
+ color: #0060e6;
1533
+ font-size: 16px;
1534
+ font-weight: bold;
1535
+ text-align: center;
1536
+ padding: 0;
1537
+ background-color: #eef5ff;
1538
+ border: 0;
1539
+ border-radius: 50%;
1540
+ cursor: pointer;
1541
+ }
1542
+
1543
+ .medplum-calendar-table button:hover {
1544
+ background-color: #d9e9ff;
1545
+ }
1546
+
1547
+ .medplum-calendar-table button:disabled {
1548
+ background-color: white;
1549
+ cursor: default;
1550
+ color: #666;
1551
+ font-weight: normal;
1552
+ }
1553
+
1554
+ .medplum-calendar-container {
1555
+ display: flex;
1556
+ min-height: 400px;
1557
+ }
1558
+
1559
+ .medplum-calendar-info-pane {
1560
+ min-width: 300px;
1561
+ padding: 20px;
1562
+ border-right: 1px solid #ddd;
1563
+ }
1564
+
1565
+ .medplum-calendar-selection-pane {
1566
+ min-width: 300px;
1567
+ padding: 20px;
1568
+ }
1569
+
1570
+ .grecaptcha-badge { visibility: hidden; }
1571
+
1514
1572
  .medplum-signin {
1515
1573
  max-width: 400px;
1516
1574
  }
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { formatAddress, getDisplayString, getImageSrc, capitalize, globalSchema, getPropertyDisplayName, formatHumanName, stringify, buildTypeName, PropertyType, getTypedPropertyValue, createReference, toTypedValue, getReferenceString, evalFhirPath, getSearchParameterDetails, Operator, evalFhirPathTyped, SearchParameterType, formatSearchQuery, parseSearchDefinition, DEFAULT_SEARCH_COUNT, isUUID } from '@medplum/core';
2
- import React, { useState, useRef, createContext, useEffect, useContext, useCallback } from 'react';
2
+ import React, { useState, useRef, createContext, useEffect, useContext, useCallback, useMemo } from 'react';
3
3
  import { useNavigate, useLocation } from 'react-router-dom';
4
4
 
5
5
  function AddressDisplay(props) {
@@ -5265,6 +5265,165 @@ function getVersionUrl(resource) {
5265
5265
  return `/${resource.resourceType}/${resource.id}/_history/${(_a = resource.meta) === null || _a === void 0 ? void 0 : _a.versionId}`;
5266
5266
  }
5267
5267
 
5268
+ /**
5269
+ * Returns a month display string (e.g. "January 2020").
5270
+ * @param date Any date within the month.
5271
+ * @returns The month display string (e.g. "January 2020")
5272
+ */
5273
+ function getMonthString(date) {
5274
+ return date.toLocaleString('default', { month: 'long' }) + ' ' + date.getFullYear();
5275
+ }
5276
+ function CalendarInput(props) {
5277
+ const [month, setMonth] = useState(getStartMonth);
5278
+ function moveMonth(delta) {
5279
+ setMonth((currMonth) => {
5280
+ const prevMonth = new Date(currMonth.getTime());
5281
+ prevMonth.setMonth(currMonth.getMonth() + delta);
5282
+ return prevMonth;
5283
+ });
5284
+ }
5285
+ const grid = useMemo(() => buildGrid(month, props.slots), [month, props.slots]);
5286
+ return (React.createElement("div", null,
5287
+ React.createElement(InputRow, null,
5288
+ React.createElement("p", { style: { flex: 1 } }, getMonthString(month)),
5289
+ React.createElement("p", null,
5290
+ React.createElement(Button, { label: "Previous month", onClick: () => moveMonth(-1) }, "<"),
5291
+ React.createElement(Button, { label: "Next month", onClick: () => moveMonth(1) }, ">"))),
5292
+ React.createElement("table", { className: "medplum-calendar-table" },
5293
+ React.createElement("thead", null,
5294
+ React.createElement("tr", null,
5295
+ React.createElement("th", null, "SUN"),
5296
+ React.createElement("th", null, "MON"),
5297
+ React.createElement("th", null, "TUE"),
5298
+ React.createElement("th", null, "WED"),
5299
+ React.createElement("th", null, "THU"),
5300
+ React.createElement("th", null, "FRI"),
5301
+ React.createElement("th", null, "SAT"))),
5302
+ React.createElement("tbody", null, grid.map((week, weekIndex) => (React.createElement("tr", { key: 'week-' + weekIndex }, week.map((day, dayIndex) => (React.createElement("td", { key: 'day-' + dayIndex }, day && (React.createElement("button", { disabled: !day.available, onClick: () => props.onClick(day.date) }, day.date.getDate()))))))))))));
5303
+ }
5304
+ function getStartMonth() {
5305
+ const result = new Date();
5306
+ result.setDate(1);
5307
+ result.setHours(0, 0, 0, 0);
5308
+ return result;
5309
+ }
5310
+ function buildGrid(startDate, slots) {
5311
+ const d = new Date(startDate.getFullYear(), startDate.getMonth());
5312
+ const grid = [];
5313
+ let row = [];
5314
+ // Fill leading empty days
5315
+ for (let i = 0; i < d.getDay(); i++) {
5316
+ row.push(undefined);
5317
+ }
5318
+ while (d.getMonth() === startDate.getMonth()) {
5319
+ row.push({
5320
+ date: new Date(d.getTime()),
5321
+ // available: isAvailable(d),
5322
+ available: isDayAvailable(d, slots),
5323
+ });
5324
+ if (d.getDay() === 6) {
5325
+ grid.push(row);
5326
+ row = [];
5327
+ }
5328
+ d.setDate(d.getDate() + 1);
5329
+ }
5330
+ // Fill trailing empty days
5331
+ if (d.getDay() !== 0) {
5332
+ for (let i = d.getDay(); i < 7; i++) {
5333
+ row.push(undefined);
5334
+ }
5335
+ grid.push(row);
5336
+ }
5337
+ return grid;
5338
+ }
5339
+ /**
5340
+ * Returns true if the given date is available for booking.
5341
+ * @param day The day to check.
5342
+ * @param slots The list of available slots.
5343
+ * @returns True if there are any available slots for the day.
5344
+ */
5345
+ function isDayAvailable(day, slots) {
5346
+ // Note that slot start and end time may or may not be in UTC.
5347
+ for (const slot of slots) {
5348
+ const slotStart = new Date(slot.start);
5349
+ if (slotStart.getFullYear() === day.getFullYear() &&
5350
+ slotStart.getMonth() === day.getMonth() &&
5351
+ slotStart.getDate() === day.getDate()) {
5352
+ return true;
5353
+ }
5354
+ }
5355
+ return false;
5356
+ }
5357
+
5358
+ function Scheduler(props) {
5359
+ var _a;
5360
+ const medplum = useMedplum();
5361
+ const schedule = useResource(props.schedule);
5362
+ const [slots, setSlots] = useState();
5363
+ const slotsRef = useRef();
5364
+ slotsRef.current = slots;
5365
+ const [date, setDate] = useState();
5366
+ const [slot, setSlot] = useState();
5367
+ const [info, setInfo] = useState();
5368
+ const [form, setForm] = useState();
5369
+ useEffect(() => {
5370
+ if (schedule) {
5371
+ medplum.search('Slot', 'schedule=' + getReferenceString(schedule)).then((bundle) => {
5372
+ setSlots(bundle.entry.map((entry) => entry.resource));
5373
+ });
5374
+ }
5375
+ else {
5376
+ setSlots(undefined);
5377
+ }
5378
+ }, [medplum, schedule]);
5379
+ if (!schedule || !slots) {
5380
+ return null;
5381
+ }
5382
+ const actor = (_a = schedule.actor) === null || _a === void 0 ? void 0 : _a[0];
5383
+ return (React.createElement("div", { className: "medplum-calendar-container", "data-testid": "scheduler" },
5384
+ React.createElement("div", { className: "medplum-calendar-info-pane" },
5385
+ actor && React.createElement(Avatar, { value: actor, size: "large" }),
5386
+ actor && (React.createElement("h1", null,
5387
+ React.createElement(ResourceName, { value: actor }))),
5388
+ React.createElement("p", null, "1 hour"),
5389
+ date && React.createElement("p", null, date.toLocaleDateString()),
5390
+ slot && React.createElement("p", null, formatTime(new Date(slot.start)))),
5391
+ React.createElement("div", { className: "medplum-calendar-selection-pane" },
5392
+ !date && (React.createElement("div", null,
5393
+ React.createElement("h3", null, "Select date"),
5394
+ React.createElement(CalendarInput, { slots: slots, onClick: setDate }))),
5395
+ date && !slot && (React.createElement("div", null,
5396
+ React.createElement("h3", null, "Select time"),
5397
+ slots.map((s) => {
5398
+ const slotStart = new Date(s.start);
5399
+ return (slotStart.getTime() > date.getTime() &&
5400
+ slotStart.getTime() < date.getTime() + 24 * 3600 * 1000 && (React.createElement("div", { key: s.id },
5401
+ React.createElement(Button, { style: { width: 150 }, onClick: () => setSlot(s) }, formatTime(slotStart)))));
5402
+ }))),
5403
+ date && slot && !info && (React.createElement("div", null,
5404
+ React.createElement("h3", null, "Enter your info"),
5405
+ React.createElement(FormSection, { title: "Name", htmlFor: "name" },
5406
+ React.createElement(Input, { name: "name" })),
5407
+ React.createElement(FormSection, { title: "Email", htmlFor: "email" },
5408
+ React.createElement(Input, { name: "email" })),
5409
+ React.createElement(Button, { primary: true, onClick: () => setInfo('info') }, "Next"))),
5410
+ date && slot && info && !form && (React.createElement("div", null,
5411
+ React.createElement("h3", null, "Custom questions"),
5412
+ React.createElement(FormSection, { title: "Question 1", htmlFor: "q1" },
5413
+ React.createElement(Input, { name: "q1" })),
5414
+ React.createElement(FormSection, { title: "Question 2", htmlFor: "q2" },
5415
+ React.createElement(Input, { name: "email" })),
5416
+ React.createElement(FormSection, { title: "Question 3", htmlFor: "q3" },
5417
+ React.createElement(Input, { name: "email" })),
5418
+ React.createElement(Button, { primary: true, onClick: () => setForm('form') }, "Next"))),
5419
+ date && slot && info && form && (React.createElement("div", null,
5420
+ React.createElement("h3", null, "You're all set!"),
5421
+ React.createElement("p", null, "Check your email for a calendar invite."))))));
5422
+ }
5423
+ function formatTime(date) {
5424
+ return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
5425
+ }
5426
+
5268
5427
  function ServiceRequestTimeline(props) {
5269
5428
  return (React.createElement(ResourceTimeline, { value: props.serviceRequest, buildSearchRequests: (resource) => ({
5270
5429
  resourceType: 'Bundle',
@@ -5526,5 +5685,5 @@ function TabSwitch(props) {
5526
5685
  })));
5527
5686
  }
5528
5687
 
5529
- export { AddressDisplay, AddressInput, AttachmentArrayDisplay, AttachmentArrayInput, AttachmentInput, Autocomplete, Avatar, BackboneElementInput, Button, Checkbox, CheckboxFormSection, CodeInput, CodeableConceptDisplay, CodeableConceptInput, ContactDetailDisplay, ContactDetailInput, ContactPointDisplay, ContactPointInput, DateTimeDisplay, DateTimeInput, DefaultResourceTimeline, DiagnosticReportDisplay, Document, ElementDefinitionInputSelector, ElementDefinitionTypeInput, EncounterTimeline, ErrorBoundary, FhirPathTable, FooterLinks, Form, FormSection, Header, HumanNameDisplay, HumanNameInput, IdentifierInput, Input, Loading, Logo, MedplumLink, MedplumProvider, MemoizedFhirPathTable, MemoizedSearchControl, MenuItem, ObservationTable, PatientTimeline, PlanDefinitionBuilder, Popup, QuestionnaireBuilder, QuestionnaireForm, QuestionnaireFormItem, QuestionnaireItemType, RangeDisplay, RangeInput, ReferenceInput, RequestGroupDisplay, ResourceArrayDisplay, ResourceArrayInput, ResourceBadge, ResourceBlame, ResourceDiff, ResourceForm, ResourceHistoryTable, ResourceInput, ResourceName, ResourcePropertyDisplay, ResourcePropertyInput, ResourceTable, ResourceTimeline, Scrollable, SearchChangeEvent, SearchClickEvent, SearchControl, SearchFieldEditor, SearchFilterEditor, SearchLoadEvent, Select, ServiceRequestTimeline, SignInForm, StatusBadge, Tab, TabList, TabPanel, TabSwitch, TextArea, Timeline, TimelineItem, TitleBar, UploadButton, addDateEqualsFilter, addDateFilter, addDateFilterBetween, addField, addFilter, addLastMonthFilter, addMissingFilter, addNextMonthFilter, addQuestionnaireInitialValues, addThisMonthFilter, addTodayFilter, addTomorrowFilter, addYearToDateFilter, addYesterdayFilter, buildFieldNameString, clearFilters, clearFiltersOnField, convertIsoToLocal, convertLocalToIso, createScriptTag, deleteFilter, formatRangeString, getOpString, getSearchOperators, getSortField, getTimeString, getValueAndType, hasFilterOnField, isChoiceQuestion, isSortDescending, movePage, parseForm, renderValue, setFilters, setOffset, setPropertyValue, setSort, sortByDateAndPriority, toggleSort, useMedplum, useMedplumContext, useMedplumProfile, useResource };
5688
+ export { AddressDisplay, AddressInput, AttachmentArrayDisplay, AttachmentArrayInput, AttachmentInput, Autocomplete, Avatar, BackboneElementInput, Button, Checkbox, CheckboxFormSection, CodeInput, CodeableConceptDisplay, CodeableConceptInput, ContactDetailDisplay, ContactDetailInput, ContactPointDisplay, ContactPointInput, DateTimeDisplay, DateTimeInput, DefaultResourceTimeline, DiagnosticReportDisplay, Document, ElementDefinitionInputSelector, ElementDefinitionTypeInput, EncounterTimeline, ErrorBoundary, FhirPathTable, FooterLinks, Form, FormSection, Header, HumanNameDisplay, HumanNameInput, IdentifierInput, Input, Loading, Logo, MedplumLink, MedplumProvider, MemoizedFhirPathTable, MemoizedSearchControl, MenuItem, ObservationTable, PatientTimeline, PlanDefinitionBuilder, Popup, QuestionnaireBuilder, QuestionnaireForm, QuestionnaireFormItem, QuestionnaireItemType, RangeDisplay, RangeInput, ReferenceInput, RequestGroupDisplay, ResourceArrayDisplay, ResourceArrayInput, ResourceBadge, ResourceBlame, ResourceDiff, ResourceForm, ResourceHistoryTable, ResourceInput, ResourceName, ResourcePropertyDisplay, ResourcePropertyInput, ResourceTable, ResourceTimeline, Scheduler, Scrollable, SearchChangeEvent, SearchClickEvent, SearchControl, SearchFieldEditor, SearchFilterEditor, SearchLoadEvent, Select, ServiceRequestTimeline, SignInForm, StatusBadge, Tab, TabList, TabPanel, TabSwitch, TextArea, Timeline, TimelineItem, TitleBar, UploadButton, addDateEqualsFilter, addDateFilter, addDateFilterBetween, addField, addFilter, addLastMonthFilter, addMissingFilter, addNextMonthFilter, addQuestionnaireInitialValues, addThisMonthFilter, addTodayFilter, addTomorrowFilter, addYearToDateFilter, addYesterdayFilter, buildFieldNameString, clearFilters, clearFiltersOnField, convertIsoToLocal, convertLocalToIso, createScriptTag, deleteFilter, formatRangeString, getOpString, getSearchOperators, getSortField, getTimeString, getValueAndType, hasFilterOnField, isChoiceQuestion, isSortDescending, movePage, parseForm, renderValue, setFilters, setOffset, setPropertyValue, setSort, sortByDateAndPriority, toggleSort, useMedplum, useMedplumContext, useMedplumProfile, useResource };
5530
5689
  //# sourceMappingURL=index.js.map