@medplum/react 0.9.20 → 0.9.23

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.
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
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';
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, parseJWTPayload } from '@medplum/core';
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) {
@@ -1770,18 +1770,24 @@ function ResourceBadge(props) {
1770
1770
  }
1771
1771
 
1772
1772
  function DiagnosticReportDisplay(props) {
1773
- var _a;
1773
+ var _a, _b;
1774
1774
  const diagnosticReport = useResource(props.value);
1775
+ const specimen = useResource((_a = diagnosticReport === null || diagnosticReport === void 0 ? void 0 : diagnosticReport.specimen) === null || _a === void 0 ? void 0 : _a[0]);
1775
1776
  if (!diagnosticReport) {
1776
1777
  return null;
1777
1778
  }
1778
- let textContent = undefined;
1779
+ let textContent = '';
1779
1780
  if (diagnosticReport.presentedForm && diagnosticReport.presentedForm.length > 0) {
1780
1781
  const pf = diagnosticReport.presentedForm[0];
1781
- if (((_a = pf.contentType) === null || _a === void 0 ? void 0 : _a.startsWith('text/plain')) && pf.data) {
1782
+ if (((_b = pf.contentType) === null || _b === void 0 ? void 0 : _b.startsWith('text/plain')) && pf.data) {
1782
1783
  textContent = window.atob(pf.data);
1783
1784
  }
1784
1785
  }
1786
+ if (specimen === null || specimen === void 0 ? void 0 : specimen.note) {
1787
+ for (const note of specimen.note) {
1788
+ textContent += note.text + '\n\n';
1789
+ }
1790
+ }
1785
1791
  return (React.createElement("div", { className: "medplum-diagnostic-report" },
1786
1792
  React.createElement("h1", null, "Diagnostic Report"),
1787
1793
  React.createElement("div", { className: "medplum-diagnostic-report-header" },
@@ -1801,8 +1807,8 @@ function DiagnosticReportDisplay(props) {
1801
1807
  diagnosticReport.status && (React.createElement("dl", null,
1802
1808
  React.createElement("dt", null, "Status"),
1803
1809
  React.createElement("dd", null, capitalize(diagnosticReport.status))))),
1804
- textContent && React.createElement("pre", null, textContent),
1805
- diagnosticReport.result && React.createElement(ObservationTable, { value: diagnosticReport.result })));
1810
+ diagnosticReport.result && React.createElement(ObservationTable, { value: diagnosticReport.result }),
1811
+ textContent && React.createElement("pre", null, textContent.trim())));
1806
1812
  }
1807
1813
  function ObservationTable(props) {
1808
1814
  var _a;
@@ -2178,8 +2184,7 @@ function ResourceTimeline(props) {
2178
2184
  setHistory({});
2179
2185
  return;
2180
2186
  }
2181
- const batchRequest = buildSearchRequests(resource);
2182
- medplum.post('fhir/R4', batchRequest).then(handleBatchResponse);
2187
+ medplum.executeBatch(buildSearchRequests(resource)).then(handleBatchResponse);
2183
2188
  }, [medplum, resource, buildSearchRequests]);
2184
2189
  useEffect(() => {
2185
2190
  loadTimeline();
@@ -4607,31 +4612,64 @@ function QuestionnaireFormItem(props) {
4607
4612
  return (React.createElement(TextArea, { name: name, defaultValue: initial === null || initial === void 0 ? void 0 : initial.valueString, onChange: (newValue) => onChangeAnswer({ valueString: newValue }) }));
4608
4613
  case QuestionnaireItemType.url:
4609
4614
  return (React.createElement(Input, { type: "url", name: name, defaultValue: initial === null || initial === void 0 ? void 0 : initial.valueUri, onChange: (newValue) => onChangeAnswer({ valueUri: newValue }) }));
4610
- case QuestionnaireItemType.choice:
4611
- case QuestionnaireItemType.openChoice:
4612
- return (React.createElement("div", null, item.answerOption &&
4613
- item.answerOption.map((option, index) => {
4614
- const valueElementDefinition = globalSchema.types['QuestionnaireItemAnswerOption'].properties['value[x]'];
4615
- const optionValue = getTypedPropertyValue({ type: 'QuestionnaireItemAnswerOption', value: option }, 'value');
4616
- const initialValue = getTypedPropertyValue({ type: 'QuestionnaireItemInitial', value: initial }, 'value');
4617
- const propertyName = 'value' + capitalize(optionValue.type);
4618
- const optionName = `${name}-option-${index}`;
4619
- return (React.createElement("div", { key: optionName, className: "medplum-questionnaire-option-row" },
4620
- React.createElement("div", { className: "medplum-questionnaire-option-checkbox" },
4621
- React.createElement("input", { type: "radio", id: optionName, name: name, value: optionValue.value, defaultChecked: initialValue && stringify(optionValue) === stringify(initialValue), onChange: () => onChangeAnswer({ [propertyName]: optionValue.value }) })),
4622
- React.createElement("div", null,
4623
- React.createElement("label", { htmlFor: optionName },
4624
- React.createElement(ResourcePropertyDisplay, { property: valueElementDefinition, propertyType: optionValue.type, value: optionValue.value })))));
4625
- })));
4626
4615
  case QuestionnaireItemType.attachment:
4627
4616
  return (React.createElement(AttachmentInput, { name: name, defaultValue: initial === null || initial === void 0 ? void 0 : initial.valueAttachment, onChange: (newValue) => onChangeAnswer({ valueAttachment: newValue }) }));
4628
4617
  case QuestionnaireItemType.reference:
4629
4618
  return (React.createElement(ReferenceInput, { name: name, defaultValue: initial === null || initial === void 0 ? void 0 : initial.valueReference, onChange: (newValue) => onChangeAnswer({ valueReference: newValue }) }));
4630
4619
  case QuestionnaireItemType.quantity:
4631
4620
  return (React.createElement(QuantityInput, { name: name, defaultValue: initial === null || initial === void 0 ? void 0 : initial.valueQuantity, onChange: (newValue) => onChangeAnswer({ valueQuantity: newValue }) }));
4621
+ case QuestionnaireItemType.choice:
4622
+ case QuestionnaireItemType.openChoice:
4623
+ if (isDropDownChoice(item)) {
4624
+ return (React.createElement(QuestionnaireChoiceDropDownInput, { name: name, item: item, initial: initial, onChangeAnswer: onChangeAnswer }));
4625
+ }
4626
+ else {
4627
+ return (React.createElement(QuestionnaireChoiceRadioInput, { name: name, item: item, initial: initial, onChangeAnswer: onChangeAnswer }));
4628
+ }
4632
4629
  }
4633
4630
  return null;
4634
4631
  }
4632
+ function QuestionnaireChoiceDropDownInput(props) {
4633
+ const { name, item, initial } = props;
4634
+ const valueElementDefinition = globalSchema.types['QuestionnaireItemAnswerOption'].properties['value[x]'];
4635
+ const initialValue = getTypedPropertyValue({ type: 'QuestionnaireItemInitial', value: initial }, 'value');
4636
+ return (React.createElement("select", { id: name, name: name, className: "medplum-select", onChange: (e) => {
4637
+ const index = e.currentTarget.selectedIndex;
4638
+ if (index === 0) {
4639
+ props.onChangeAnswer({});
4640
+ return;
4641
+ }
4642
+ const option = item.answerOption[index - 1];
4643
+ const optionValue = getTypedPropertyValue({ type: 'QuestionnaireItemAnswerOption', value: option }, 'value');
4644
+ const propertyName = 'value' + capitalize(optionValue.type);
4645
+ props.onChangeAnswer({ [propertyName]: optionValue.value });
4646
+ } },
4647
+ React.createElement("option", null),
4648
+ item.answerOption &&
4649
+ item.answerOption.map((option, index) => {
4650
+ const optionValue = getTypedPropertyValue({ type: 'QuestionnaireItemAnswerOption', value: option }, 'value');
4651
+ const optionName = `${name}-option-${index}`;
4652
+ return (React.createElement("option", { key: optionName, value: optionValue.value, selected: initialValue && stringify(optionValue) === stringify(initialValue) },
4653
+ React.createElement(ResourcePropertyDisplay, { property: valueElementDefinition, propertyType: optionValue.type, value: optionValue.value })));
4654
+ })));
4655
+ }
4656
+ function QuestionnaireChoiceRadioInput(props) {
4657
+ const { name, item, initial, onChangeAnswer } = props;
4658
+ const valueElementDefinition = globalSchema.types['QuestionnaireItemAnswerOption'].properties['value[x]'];
4659
+ const initialValue = getTypedPropertyValue({ type: 'QuestionnaireItemInitial', value: initial }, 'value');
4660
+ return (React.createElement(React.Fragment, null, item.answerOption &&
4661
+ item.answerOption.map((option, index) => {
4662
+ const optionValue = getTypedPropertyValue({ type: 'QuestionnaireItemAnswerOption', value: option }, 'value');
4663
+ const propertyName = 'value' + capitalize(optionValue.type);
4664
+ const optionName = `${name}-option-${index}`;
4665
+ return (React.createElement("div", { key: optionName, className: "medplum-questionnaire-option-row" },
4666
+ React.createElement("div", { className: "medplum-questionnaire-option-checkbox" },
4667
+ React.createElement("input", { type: "radio", id: optionName, name: name, value: optionValue.value, defaultChecked: initialValue && stringify(optionValue) === stringify(initialValue), onChange: () => onChangeAnswer({ [propertyName]: optionValue.value }) })),
4668
+ React.createElement("div", null,
4669
+ React.createElement("label", { htmlFor: optionName },
4670
+ React.createElement(ResourcePropertyDisplay, { property: valueElementDefinition, propertyType: optionValue.type, value: optionValue.value })))));
4671
+ })));
4672
+ }
4635
4673
  function buildInitialResponse(questionnaire) {
4636
4674
  const response = {
4637
4675
  resourceType: 'QuestionnaireResponse',
@@ -4657,6 +4695,14 @@ function buildInitialResponseAnswer(answer) {
4657
4695
  // have the same properties.
4658
4696
  return Object.assign({}, answer);
4659
4697
  }
4698
+ function isDropDownChoice(item) {
4699
+ var _a;
4700
+ return !!((_a = item.extension) === null || _a === void 0 ? void 0 : _a.some((e) => {
4701
+ var _a, _b, _c;
4702
+ return e.url === 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl' &&
4703
+ ((_c = (_b = (_a = e.valueCodeableConcept) === null || _a === void 0 ? void 0 : _a.coding) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.code) === 'drop-down';
4704
+ }));
4705
+ }
4660
4706
 
4661
4707
  function QuestionnaireBuilder(props) {
4662
4708
  const medplum = useMedplum();
@@ -4858,6 +4904,163 @@ function ensureQuestionnaireOptionKeys(options) {
4858
4904
  return options.map((option) => (Object.assign(Object.assign({}, option), { id: option.id || generateId() })));
4859
4905
  }
4860
4906
 
4907
+ /**
4908
+ * Dynamically creates a script tag for the specified JavaScript file.
4909
+ * @param src The JavaScript file URL.
4910
+ */
4911
+ function createScriptTag(src, onload) {
4912
+ const head = document.getElementsByTagName('head')[0];
4913
+ const script = document.createElement('script');
4914
+ script.async = true;
4915
+ script.src = src;
4916
+ script.onload = onload || null;
4917
+ head.appendChild(script);
4918
+ }
4919
+
4920
+ function GoogleButton(props) {
4921
+ const medplum = useMedplum();
4922
+ const { googleClientId, handleGoogleCredential } = props;
4923
+ const parentRef = useRef(null);
4924
+ const [scriptLoaded, setScriptLoaded] = useState(typeof google !== 'undefined');
4925
+ const [initialized, setInitialized] = useState(false);
4926
+ const [buttonRendered, setButtonRendered] = useState(false);
4927
+ useEffect(() => {
4928
+ if (typeof google === 'undefined') {
4929
+ createScriptTag('https://accounts.google.com/gsi/client', () => setScriptLoaded(true));
4930
+ return;
4931
+ }
4932
+ if (!initialized) {
4933
+ google.accounts.id.initialize({
4934
+ client_id: googleClientId,
4935
+ callback: handleGoogleCredential,
4936
+ });
4937
+ setInitialized(true);
4938
+ }
4939
+ if (parentRef.current && !buttonRendered) {
4940
+ google.accounts.id.renderButton(parentRef.current, {});
4941
+ setButtonRendered(true);
4942
+ }
4943
+ }, [medplum, googleClientId, initialized, scriptLoaded, parentRef, buttonRendered, handleGoogleCredential]);
4944
+ if (!googleClientId) {
4945
+ return null;
4946
+ }
4947
+ return React.createElement("div", { ref: parentRef });
4948
+ }
4949
+ function getGoogleClientId(clientId) {
4950
+ var _a, _b;
4951
+ if (clientId) {
4952
+ return clientId;
4953
+ }
4954
+ const origin = window.location.protocol + '//' + window.location.host;
4955
+ const authorizedOrigins = (_b = (_a = "http://localhost:3000,http://localhost:6006,https://app.medplum.com,https://docs.medplum.com") === null || _a === void 0 ? void 0 : _a.split(',')) !== null && _b !== void 0 ? _b : [];
4956
+ if (authorizedOrigins.includes(origin)) {
4957
+ return "921088377005-3j1sa10vr6hj86jgmdfh2l53v3mp7lfi.apps.googleusercontent.com";
4958
+ }
4959
+ return undefined;
4960
+ }
4961
+
4962
+ /**
4963
+ * Dynamically loads the recaptcha script.
4964
+ * We do not want to load the script on page load unless the user needs it.
4965
+ */
4966
+ function initRecaptcha() {
4967
+ if (typeof grecaptcha === 'undefined') {
4968
+ createScriptTag('https://www.google.com/recaptcha/api.js?render=' + process.env.RECAPTCHA_SITE_KEY);
4969
+ }
4970
+ }
4971
+ /**
4972
+ * Starts a request to generate a recapcha token.
4973
+ * @returns Promise to a recaptcha token for the current user.
4974
+ */
4975
+ function getRecaptcha() {
4976
+ return new Promise((resolve) => {
4977
+ grecaptcha.ready(() => {
4978
+ grecaptcha.execute(process.env.RECAPTCHA_SITE_KEY, { action: 'submit' }).then(resolve);
4979
+ });
4980
+ });
4981
+ }
4982
+
4983
+ function RegisterForm(props) {
4984
+ const medplum = useMedplum();
4985
+ const googleClientId = getGoogleClientId(props.googleClientId);
4986
+ const [outcome, setOutcome] = useState();
4987
+ const issues = getIssuesForExpression(outcome, undefined);
4988
+ useEffect(initRecaptcha, []);
4989
+ function handleAuthResponse(registerRequest, partialLogin) {
4990
+ return __awaiter(this, void 0, void 0, function* () {
4991
+ try {
4992
+ let login;
4993
+ if (props.type === 'patient') {
4994
+ login = yield medplum.startNewPatient(registerRequest, partialLogin);
4995
+ }
4996
+ else {
4997
+ login = yield medplum.startNewProject(registerRequest, partialLogin);
4998
+ }
4999
+ yield medplum.processCode(login.code);
5000
+ props.onSuccess();
5001
+ }
5002
+ catch (err) {
5003
+ setOutcome(err);
5004
+ }
5005
+ });
5006
+ }
5007
+ return (React.createElement(Document, { width: 450 },
5008
+ React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: (formData) => __awaiter(this, void 0, void 0, function* () {
5009
+ try {
5010
+ const recaptchaToken = yield getRecaptcha();
5011
+ const registerRequest = Object.assign(Object.assign({}, formData), { recaptchaToken });
5012
+ const userLogin = yield medplum.startNewUser(registerRequest);
5013
+ handleAuthResponse(registerRequest, userLogin);
5014
+ }
5015
+ catch (err) {
5016
+ setOutcome(err);
5017
+ }
5018
+ }) },
5019
+ React.createElement("div", { className: "medplum-center" }, props.children),
5020
+ issues && (React.createElement("div", { className: "medplum-input-error" }, issues.map((issue) => {
5021
+ var _a, _b;
5022
+ return (React.createElement("div", { "data-testid": "text-field-error", key: (_a = issue.details) === null || _a === void 0 ? void 0 : _a.text }, (_b = issue.details) === null || _b === void 0 ? void 0 : _b.text));
5023
+ }))),
5024
+ googleClientId && (React.createElement(React.Fragment, null,
5025
+ React.createElement("div", { className: "medplum-signin-google-container" },
5026
+ React.createElement(GoogleButton, { googleClientId: googleClientId, handleGoogleCredential: (response) => __awaiter(this, void 0, void 0, function* () {
5027
+ try {
5028
+ const loginRequest = {
5029
+ googleClientId: response.clientId,
5030
+ googleCredential: response.credential,
5031
+ };
5032
+ const userLogin = yield medplum.startGoogleLogin(loginRequest);
5033
+ const googleClaims = parseJWTPayload(loginRequest.googleCredential);
5034
+ const registerRequest = {
5035
+ firstName: googleClaims.given_name,
5036
+ lastName: googleClaims.family_name,
5037
+ email: googleClaims.email,
5038
+ };
5039
+ handleAuthResponse(registerRequest, userLogin);
5040
+ }
5041
+ catch (err) {
5042
+ setOutcome(err);
5043
+ }
5044
+ }) })),
5045
+ React.createElement("div", { className: "medplum-signin-separator" }, "or"))),
5046
+ React.createElement(FormSection, { title: "First Name", htmlFor: "firstName", outcome: outcome },
5047
+ React.createElement(Input, { name: "firstName", type: "text", testid: "firstName", placeholder: "First name", required: true, autoFocus: true, outcome: outcome })),
5048
+ React.createElement(FormSection, { title: "Last Name", htmlFor: "lastName", outcome: outcome },
5049
+ React.createElement(Input, { name: "lastName", type: "text", testid: "lastName", placeholder: "Last name", required: true, outcome: outcome })),
5050
+ props.type === 'project' && (React.createElement(FormSection, { title: "Project Name", htmlFor: "projectName", outcome: outcome },
5051
+ React.createElement(Input, { name: "projectName", type: "text", testid: "projectName", placeholder: "My Project", required: true, outcome: outcome }))),
5052
+ React.createElement(FormSection, { title: "Email", htmlFor: "email", outcome: outcome },
5053
+ React.createElement(Input, { name: "email", type: "email", testid: "email", placeholder: "name@domain.com", required: true, outcome: outcome })),
5054
+ React.createElement(FormSection, { title: "Password", htmlFor: "password", outcome: outcome },
5055
+ React.createElement(Input, { name: "password", type: "password", testid: "password", autoComplete: "off", required: true, outcome: outcome })),
5056
+ React.createElement("div", { className: "medplum-signin-buttons" },
5057
+ React.createElement("div", null,
5058
+ React.createElement("input", { type: "checkbox", id: "remember", name: "remember", value: "true" }),
5059
+ React.createElement("label", { htmlFor: "remember" }, "Remember me")),
5060
+ React.createElement("div", null,
5061
+ React.createElement(Button, { type: "submit", testid: "submit" }, "Create account"))))));
5062
+ }
5063
+
4861
5064
  function StatusBadge(props) {
4862
5065
  return React.createElement("span", { className: `medplum-status medplum-status-${props.status}` }, props.status);
4863
5066
  }
@@ -4870,7 +5073,7 @@ function RequestGroupDisplay(props) {
4870
5073
  const [responseBundle, setResponseBundle] = useState();
4871
5074
  useEffect(() => {
4872
5075
  if (requestGroup && !startedLoading) {
4873
- medplum.post('fhir/R4', buildBatchRequest(requestGroup)).then(setResponseBundle);
5076
+ medplum.executeBatch(buildBatchRequest(requestGroup)).then(setResponseBundle);
4874
5077
  setStartedLoading(true);
4875
5078
  }
4876
5079
  }, [medplum, requestGroup, startedLoading]);
@@ -5225,6 +5428,165 @@ function getVersionUrl(resource) {
5225
5428
  return `/${resource.resourceType}/${resource.id}/_history/${(_a = resource.meta) === null || _a === void 0 ? void 0 : _a.versionId}`;
5226
5429
  }
5227
5430
 
5431
+ /**
5432
+ * Returns a month display string (e.g. "January 2020").
5433
+ * @param date Any date within the month.
5434
+ * @returns The month display string (e.g. "January 2020")
5435
+ */
5436
+ function getMonthString(date) {
5437
+ return date.toLocaleString('default', { month: 'long' }) + ' ' + date.getFullYear();
5438
+ }
5439
+ function CalendarInput(props) {
5440
+ const [month, setMonth] = useState(getStartMonth);
5441
+ function moveMonth(delta) {
5442
+ setMonth((currMonth) => {
5443
+ const prevMonth = new Date(currMonth.getTime());
5444
+ prevMonth.setMonth(currMonth.getMonth() + delta);
5445
+ return prevMonth;
5446
+ });
5447
+ }
5448
+ const grid = useMemo(() => buildGrid(month, props.slots), [month, props.slots]);
5449
+ return (React.createElement("div", null,
5450
+ React.createElement(InputRow, null,
5451
+ React.createElement("p", { style: { flex: 1 } }, getMonthString(month)),
5452
+ React.createElement("p", null,
5453
+ React.createElement(Button, { label: "Previous month", onClick: () => moveMonth(-1) }, "<"),
5454
+ React.createElement(Button, { label: "Next month", onClick: () => moveMonth(1) }, ">"))),
5455
+ React.createElement("table", { className: "medplum-calendar-table" },
5456
+ React.createElement("thead", null,
5457
+ React.createElement("tr", null,
5458
+ React.createElement("th", null, "SUN"),
5459
+ React.createElement("th", null, "MON"),
5460
+ React.createElement("th", null, "TUE"),
5461
+ React.createElement("th", null, "WED"),
5462
+ React.createElement("th", null, "THU"),
5463
+ React.createElement("th", null, "FRI"),
5464
+ React.createElement("th", null, "SAT"))),
5465
+ 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()))))))))))));
5466
+ }
5467
+ function getStartMonth() {
5468
+ const result = new Date();
5469
+ result.setDate(1);
5470
+ result.setHours(0, 0, 0, 0);
5471
+ return result;
5472
+ }
5473
+ function buildGrid(startDate, slots) {
5474
+ const d = new Date(startDate.getFullYear(), startDate.getMonth());
5475
+ const grid = [];
5476
+ let row = [];
5477
+ // Fill leading empty days
5478
+ for (let i = 0; i < d.getDay(); i++) {
5479
+ row.push(undefined);
5480
+ }
5481
+ while (d.getMonth() === startDate.getMonth()) {
5482
+ row.push({
5483
+ date: new Date(d.getTime()),
5484
+ // available: isAvailable(d),
5485
+ available: isDayAvailable(d, slots),
5486
+ });
5487
+ if (d.getDay() === 6) {
5488
+ grid.push(row);
5489
+ row = [];
5490
+ }
5491
+ d.setDate(d.getDate() + 1);
5492
+ }
5493
+ // Fill trailing empty days
5494
+ if (d.getDay() !== 0) {
5495
+ for (let i = d.getDay(); i < 7; i++) {
5496
+ row.push(undefined);
5497
+ }
5498
+ grid.push(row);
5499
+ }
5500
+ return grid;
5501
+ }
5502
+ /**
5503
+ * Returns true if the given date is available for booking.
5504
+ * @param day The day to check.
5505
+ * @param slots The list of available slots.
5506
+ * @returns True if there are any available slots for the day.
5507
+ */
5508
+ function isDayAvailable(day, slots) {
5509
+ // Note that slot start and end time may or may not be in UTC.
5510
+ for (const slot of slots) {
5511
+ const slotStart = new Date(slot.start);
5512
+ if (slotStart.getFullYear() === day.getFullYear() &&
5513
+ slotStart.getMonth() === day.getMonth() &&
5514
+ slotStart.getDate() === day.getDate()) {
5515
+ return true;
5516
+ }
5517
+ }
5518
+ return false;
5519
+ }
5520
+
5521
+ function Scheduler(props) {
5522
+ var _a;
5523
+ const medplum = useMedplum();
5524
+ const schedule = useResource(props.schedule);
5525
+ const [slots, setSlots] = useState();
5526
+ const slotsRef = useRef();
5527
+ slotsRef.current = slots;
5528
+ const [date, setDate] = useState();
5529
+ const [slot, setSlot] = useState();
5530
+ const [info, setInfo] = useState();
5531
+ const [form, setForm] = useState();
5532
+ useEffect(() => {
5533
+ if (schedule) {
5534
+ medplum.search('Slot', 'schedule=' + getReferenceString(schedule)).then((bundle) => {
5535
+ setSlots(bundle.entry.map((entry) => entry.resource));
5536
+ });
5537
+ }
5538
+ else {
5539
+ setSlots(undefined);
5540
+ }
5541
+ }, [medplum, schedule]);
5542
+ if (!schedule || !slots) {
5543
+ return null;
5544
+ }
5545
+ const actor = (_a = schedule.actor) === null || _a === void 0 ? void 0 : _a[0];
5546
+ return (React.createElement("div", { className: "medplum-calendar-container", "data-testid": "scheduler" },
5547
+ React.createElement("div", { className: "medplum-calendar-info-pane" },
5548
+ actor && React.createElement(Avatar, { value: actor, size: "large" }),
5549
+ actor && (React.createElement("h1", null,
5550
+ React.createElement(ResourceName, { value: actor }))),
5551
+ React.createElement("p", null, "1 hour"),
5552
+ date && React.createElement("p", null, date.toLocaleDateString()),
5553
+ slot && React.createElement("p", null, formatTime(new Date(slot.start)))),
5554
+ React.createElement("div", { className: "medplum-calendar-selection-pane" },
5555
+ !date && (React.createElement("div", null,
5556
+ React.createElement("h3", null, "Select date"),
5557
+ React.createElement(CalendarInput, { slots: slots, onClick: setDate }))),
5558
+ date && !slot && (React.createElement("div", null,
5559
+ React.createElement("h3", null, "Select time"),
5560
+ slots.map((s) => {
5561
+ const slotStart = new Date(s.start);
5562
+ return (slotStart.getTime() > date.getTime() &&
5563
+ slotStart.getTime() < date.getTime() + 24 * 3600 * 1000 && (React.createElement("div", { key: s.id },
5564
+ React.createElement(Button, { style: { width: 150 }, onClick: () => setSlot(s) }, formatTime(slotStart)))));
5565
+ }))),
5566
+ date && slot && !info && (React.createElement("div", null,
5567
+ React.createElement("h3", null, "Enter your info"),
5568
+ React.createElement(FormSection, { title: "Name", htmlFor: "name" },
5569
+ React.createElement(Input, { name: "name" })),
5570
+ React.createElement(FormSection, { title: "Email", htmlFor: "email" },
5571
+ React.createElement(Input, { name: "email" })),
5572
+ React.createElement(Button, { primary: true, onClick: () => setInfo('info') }, "Next"))),
5573
+ date && slot && info && !form && (React.createElement("div", null,
5574
+ React.createElement("h3", null, "Custom questions"),
5575
+ React.createElement(FormSection, { title: "Question 1", htmlFor: "q1" },
5576
+ React.createElement(Input, { name: "q1" })),
5577
+ React.createElement(FormSection, { title: "Question 2", htmlFor: "q2" },
5578
+ React.createElement(Input, { name: "email" })),
5579
+ React.createElement(FormSection, { title: "Question 3", htmlFor: "q3" },
5580
+ React.createElement(Input, { name: "email" })),
5581
+ React.createElement(Button, { primary: true, onClick: () => setForm('form') }, "Next"))),
5582
+ date && slot && info && form && (React.createElement("div", null,
5583
+ React.createElement("h3", null, "You're all set!"),
5584
+ React.createElement("p", null, "Check your email for a calendar invite."))))));
5585
+ }
5586
+ function formatTime(date) {
5587
+ return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
5588
+ }
5589
+
5228
5590
  function ServiceRequestTimeline(props) {
5229
5591
  return (React.createElement(ResourceTimeline, { value: props.serviceRequest, buildSearchRequests: (resource) => ({
5230
5592
  resourceType: 'Bundle',
@@ -5272,62 +5634,6 @@ function ServiceRequestTimeline(props) {
5272
5634
  }) }));
5273
5635
  }
5274
5636
 
5275
- /**
5276
- * Dynamically creates a script tag for the specified JavaScript file.
5277
- * @param src The JavaScript file URL.
5278
- */
5279
- function createScriptTag(src, onload) {
5280
- const head = document.getElementsByTagName('head')[0];
5281
- const script = document.createElement('script');
5282
- script.async = true;
5283
- script.src = src;
5284
- script.onload = onload || null;
5285
- head.appendChild(script);
5286
- }
5287
-
5288
- function GoogleButton(props) {
5289
- const medplum = useMedplum();
5290
- const { handleGoogleCredential } = props;
5291
- const googleClientId = getGoogleClientId(props.googleClientId);
5292
- const parentRef = useRef(null);
5293
- const [scriptLoaded, setScriptLoaded] = useState(typeof google !== 'undefined');
5294
- const [initialized, setInitialized] = useState(false);
5295
- const [buttonRendered, setButtonRendered] = useState(false);
5296
- useEffect(() => {
5297
- if (typeof google === 'undefined') {
5298
- createScriptTag('https://accounts.google.com/gsi/client', () => setScriptLoaded(true));
5299
- return;
5300
- }
5301
- if (!initialized) {
5302
- google.accounts.id.initialize({
5303
- client_id: googleClientId,
5304
- callback: handleGoogleCredential,
5305
- });
5306
- setInitialized(true);
5307
- }
5308
- if (parentRef.current && !buttonRendered) {
5309
- google.accounts.id.renderButton(parentRef.current, {});
5310
- setButtonRendered(true);
5311
- }
5312
- }, [medplum, googleClientId, initialized, scriptLoaded, parentRef, buttonRendered, handleGoogleCredential]);
5313
- if (!googleClientId) {
5314
- return null;
5315
- }
5316
- return React.createElement("div", { ref: parentRef });
5317
- }
5318
- function getGoogleClientId(clientId) {
5319
- var _a, _b;
5320
- if (clientId) {
5321
- return clientId;
5322
- }
5323
- const origin = window.location.protocol + '//' + window.location.host;
5324
- const authorizedOrigins = (_b = (_a = "http://localhost:3000,http://localhost:6006,https://app.medplum.com,https://docs.medplum.com") === null || _a === void 0 ? void 0 : _a.split(',')) !== null && _b !== void 0 ? _b : [];
5325
- if (authorizedOrigins.includes(origin)) {
5326
- return "921088377005-3j1sa10vr6hj86jgmdfh2l53v3mp7lfi.apps.googleusercontent.com";
5327
- }
5328
- return undefined;
5329
- }
5330
-
5331
5637
  function SignInForm(props) {
5332
5638
  const medplum = useMedplum();
5333
5639
  const [login, setLogin] = useState(undefined);
@@ -5369,6 +5675,7 @@ function SignInForm(props) {
5369
5675
  }
5370
5676
  function AuthenticationForm(props) {
5371
5677
  const medplum = useMedplum();
5678
+ const googleClientId = getGoogleClientId(props.googleClientId);
5372
5679
  const [outcome, setOutcome] = useState();
5373
5680
  const issues = getIssuesForExpression(outcome, undefined);
5374
5681
  return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: (formData) => {
@@ -5389,6 +5696,21 @@ function AuthenticationForm(props) {
5389
5696
  var _a, _b;
5390
5697
  return (React.createElement("div", { "data-testid": "text-field-error", key: (_a = issue.details) === null || _a === void 0 ? void 0 : _a.text }, (_b = issue.details) === null || _b === void 0 ? void 0 : _b.text));
5391
5698
  }))),
5699
+ googleClientId && (React.createElement(React.Fragment, null,
5700
+ React.createElement("div", { className: "medplum-signin-google-container" },
5701
+ React.createElement(GoogleButton, { googleClientId: googleClientId, handleGoogleCredential: (response) => {
5702
+ medplum
5703
+ .startGoogleLogin({
5704
+ clientId: props.clientId,
5705
+ scope: props.scope,
5706
+ nonce: props.nonce,
5707
+ googleClientId: response.clientId,
5708
+ googleCredential: response.credential,
5709
+ })
5710
+ .then(props.handleAuthResponse)
5711
+ .catch(setOutcome);
5712
+ } })),
5713
+ React.createElement("div", { className: "medplum-signin-separator" }, "or"))),
5392
5714
  React.createElement(FormSection, { title: "Email", htmlFor: "email", outcome: outcome },
5393
5715
  React.createElement(Input, { name: "email", type: "email", testid: "email", required: true, autoFocus: true, outcome: outcome })),
5394
5716
  React.createElement(FormSection, { title: "Password", htmlFor: "password", outcome: outcome },
@@ -5401,20 +5723,7 @@ function AuthenticationForm(props) {
5401
5723
  React.createElement("input", { type: "checkbox", id: "remember", name: "remember", value: "true" }),
5402
5724
  React.createElement("label", { htmlFor: "remember" }, "Remember me")),
5403
5725
  React.createElement("div", null,
5404
- React.createElement(Button, { type: "submit", testid: "submit" }, "Sign in"))),
5405
- React.createElement("div", { className: "medplum-signin-google-container" },
5406
- React.createElement(GoogleButton, { googleClientId: props.googleClientId, handleGoogleCredential: (response) => {
5407
- medplum
5408
- .startGoogleLogin({
5409
- clientId: props.clientId,
5410
- scope: props.scope,
5411
- nonce: props.nonce,
5412
- googleClientId: response.clientId,
5413
- googleCredential: response.credential,
5414
- })
5415
- .then(props.handleAuthResponse)
5416
- .catch(setOutcome);
5417
- } }))));
5726
+ React.createElement(Button, { type: "submit", testid: "submit" }, "Sign in")))));
5418
5727
  }
5419
5728
  function ProfileForm(props) {
5420
5729
  const medplum = useMedplum();
@@ -5486,5 +5795,5 @@ function TabSwitch(props) {
5486
5795
  })));
5487
5796
  }
5488
5797
 
5489
- 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 };
5798
+ 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, RegisterForm, 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, getRecaptcha, getSearchOperators, getSortField, getTimeString, getValueAndType, hasFilterOnField, initRecaptcha, isChoiceQuestion, isSortDescending, movePage, parseForm, renderValue, setFilters, setOffset, setPropertyValue, setSort, sortByDateAndPriority, toggleSort, useMedplum, useMedplumContext, useMedplumProfile, useResource };
5490
5799
  //# sourceMappingURL=index.js.map