From 2563bfeb2d7a1bf3735c6258c076cc02a391c5dd Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Wed, 8 Aug 2018 18:01:05 +0300 Subject: [PATCH 01/11] Default time slots to null --- src/containers/ListingPage/ListingPage.duck.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/ListingPage/ListingPage.duck.js b/src/containers/ListingPage/ListingPage.duck.js index 2dafe664..9ada66a1 100644 --- a/src/containers/ListingPage/ListingPage.duck.js +++ b/src/containers/ListingPage/ListingPage.duck.js @@ -37,7 +37,7 @@ const initialState = { showListingError: null, reviews: [], fetchReviewsError: null, - timeSlots: [], + timeSlots: null, fetchTimesLotsError: null, sendEnquiryInProgress: false, sendEnquiryError: null, From bcbeec8aefdb47b5d3c6e11b9c6cada3870d7370 Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Wed, 8 Aug 2018 18:03:51 +0300 Subject: [PATCH 02/11] Pass time slots to BookingDatesForm --- src/containers/ListingPage/ListingPage.js | 2 ++ src/containers/ListingPage/SectionBooking.js | 2 ++ .../ListingPage/__snapshots__/ListingPage.test.js.snap | 1 + src/forms/BookingDatesForm/BookingDatesForm.js | 6 +++++- .../__snapshots__/BookingDatesForm.test.js.snap | 1 + 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/containers/ListingPage/ListingPage.js b/src/containers/ListingPage/ListingPage.js index 80af60e5..4d6eacc1 100644 --- a/src/containers/ListingPage/ListingPage.js +++ b/src/containers/ListingPage/ListingPage.js @@ -197,6 +197,7 @@ export class ListingPageComponent extends Component { fetchReviewsError, sendEnquiryInProgress, sendEnquiryError, + timeSlots, categoriesConfig, amenitiesConfig, } = this.props; @@ -477,6 +478,7 @@ export class ListingPageComponent extends Component { handleBookButtonClick={handleBookButtonClick} handleMobileBookModalClose={handleMobileBookModalClose} onManageDisableScrolling={onManageDisableScrolling} + timeSlots={timeSlots} /> diff --git a/src/containers/ListingPage/SectionBooking.js b/src/containers/ListingPage/SectionBooking.js index 5efa683d..08cb4a6f 100644 --- a/src/containers/ListingPage/SectionBooking.js +++ b/src/containers/ListingPage/SectionBooking.js @@ -24,6 +24,7 @@ const SectionBooking = props => { handleBookButtonClick, handleMobileBookModalClose, onManageDisableScrolling, + timeSlots, } = props; const showClosedListingHelpText = listing.id && isClosed; return ( @@ -68,6 +69,7 @@ const SectionBooking = props => { onSubmit={handleBookingSubmit} price={price} isOwnListing={isOwnListing} + timeSlots={timeSlots} /> ) : null} diff --git a/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap b/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap index ef469cdf..4ec39bef 100644 --- a/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap +++ b/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap @@ -325,6 +325,7 @@ exports[`ListingPage matches snapshot 1`] = ` } + timeSlots={null} unitType="line-item/night" /> diff --git a/src/forms/BookingDatesForm/BookingDatesForm.js b/src/forms/BookingDatesForm/BookingDatesForm.js index 285ccf92..a127a951 100644 --- a/src/forms/BookingDatesForm/BookingDatesForm.js +++ b/src/forms/BookingDatesForm/BookingDatesForm.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { string, bool } from 'prop-types'; +import { string, bool, arrayOf } from 'prop-types'; import { compose } from 'redux'; import { Form as FinalForm } from 'react-final-form'; import { FormattedMessage, intlShape, injectIntl } from 'react-intl'; @@ -85,6 +85,7 @@ export class BookingDatesFormComponent extends Component { unitPrice, unitType, values, + timeSlots, } = fieldRenderProps; const { startDate, endDate } = values && values.bookingDates ? values.bookingDates : {}; @@ -160,6 +161,7 @@ export class BookingDatesFormComponent extends Component { focusedInput={this.state.focusedInput} onFocusedInputChange={this.onFocusedInputChange} format={null} + timeSlots={timeSlots} useMobileMargins validate={composeValidators( required(requiredMessage), @@ -197,6 +199,7 @@ BookingDatesFormComponent.defaultProps = { isOwnListing: false, startDatePlaceholder: null, endDatePlaceholder: null, + timeSlots: null, }; BookingDatesFormComponent.propTypes = { @@ -207,6 +210,7 @@ BookingDatesFormComponent.propTypes = { unitType: propTypes.bookingUnitType.isRequired, price: propTypes.money, isOwnListing: bool, + timeSlots: arrayOf(propTypes.timeSlot), // from injectIntl intl: intlShape.isRequired, diff --git a/src/forms/BookingDatesForm/__snapshots__/BookingDatesForm.test.js.snap b/src/forms/BookingDatesForm/__snapshots__/BookingDatesForm.test.js.snap index 7a310e10..e9b67861 100644 --- a/src/forms/BookingDatesForm/__snapshots__/BookingDatesForm.test.js.snap +++ b/src/forms/BookingDatesForm/__snapshots__/BookingDatesForm.test.js.snap @@ -22,6 +22,7 @@ exports[`BookingDatesForm matches snapshot without selected dates 1`] = ` render={[Function]} startDatePlaceholder="today" submitButtonWrapperClassName={null} + timeSlots={null} unitPrice={ Money { "amount": 1099, From 1706f22e52e338303eeb3645b373cb0a7c798883 Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Wed, 8 Aug 2018 18:41:38 +0300 Subject: [PATCH 03/11] Add error handling to checkout page Add handling for the tx time already booked error to checkout page. --- src/containers/CheckoutPage/CheckoutPage.js | 7 +++++++ src/translations/en.json | 1 + src/util/errors.js | 9 +++++++++ src/util/types.js | 2 ++ 4 files changed, 19 insertions(+) diff --git a/src/containers/CheckoutPage/CheckoutPage.js b/src/containers/CheckoutPage/CheckoutPage.js index 7581ff5c..7a3f91e6 100644 --- a/src/containers/CheckoutPage/CheckoutPage.js +++ b/src/containers/CheckoutPage/CheckoutPage.js @@ -15,6 +15,7 @@ import { isTransactionInitiateAmountTooLowError, isTransactionInitiateListingNotFoundError, isTransactionInitiateMissingStripeAccountError, + isTransactionInitiateBookingTimeNotAvailableError, } from '../../util/errors'; import { AvatarMedium, @@ -298,6 +299,12 @@ export class CheckoutPageComponent extends Component {

); + } else if (isTransactionInitiateBookingTimeNotAvailableError(speculateTransactionError)) { + speculateErrorMessage = ( +

+ +

+ ); } else if (speculateTransactionError) { speculateErrorMessage = (

diff --git a/src/translations/en.json b/src/translations/en.json index 06b662aa..15cacf57 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -66,6 +66,7 @@ "BookingDatesForm.requestToBook": "Request to book", "BookingDatesForm.requiredDate": "Oops, make sure your date is correct!", "BookingDatesForm.youWontBeChargedInfo": "You won't be charged yet", + "CheckoutPage.bookingTimeNotAvailableMessage": "Unfortunately, the requested time is already booked.", "CheckoutPage.errorlistingLinkText": "the sauna page", "CheckoutPage.goToLandingPage": "Go to homepage", "CheckoutPage.hostedBy": "Hosted by {name}", diff --git a/src/util/errors.js b/src/util/errors.js index 3fd07d8f..732d52bd 100644 --- a/src/util/errors.js +++ b/src/util/errors.js @@ -21,6 +21,7 @@ import { ERROR_CODE_TOO_MANY_VERIFICATION_REQUESTS, ERROR_CODE_UPLOAD_OVER_LIMIT, ERROR_CODE_MISSING_STRIPE_ACCOUNT, + ERROR_CODE_TRANSACTION_BOOKING_TIME_NOT_AVAILABLE, } from './types'; const errorAPIErrors = error => { @@ -102,6 +103,14 @@ export const isTransactionInitiateListingNotFoundError = error => export const isTransactionInitiateMissingStripeAccountError = error => hasErrorWithCode(error, ERROR_CODE_MISSING_STRIPE_ACCOUNT); +/** + * Check if the given API error (from `sdk.transaction.initiate()` or + * `sdk.transaction.initiateSpeculative()`) is due to selected booking + * time already being booked. + */ +export const isTransactionInitiateBookingTimeNotAvailableError = error => + hasErrorWithCode(error, ERROR_CODE_TRANSACTION_BOOKING_TIME_NOT_AVAILABLE); + /** * Check if the given API error (from `sdk.transaction.initiate()`) is * due to the transaction total amount being too low for Stripe. diff --git a/src/util/types.js b/src/util/types.js index b06a3962..cd4ca53f 100644 --- a/src/util/types.js +++ b/src/util/types.js @@ -432,6 +432,8 @@ export const ERROR_CODE_TRANSACTION_ALREADY_REVIEWED_BY_CUSTOMER = 'transaction-already-reviewed-by-customer'; export const ERROR_CODE_TRANSACTION_ALREADY_REVIEWED_BY_PROVIDER = 'transaction-already-reviewed-by-provider'; +export const ERROR_CODE_TRANSACTION_BOOKING_TIME_NOT_AVAILABLE = + 'transaction-booking-time-not-available'; export const ERROR_CODE_PAYMENT_FAILED = 'transaction-payment-failed'; export const ERROR_CODE_EMAIL_TAKEN = 'email-taken'; export const ERROR_CODE_EMAIL_NOT_FOUND = 'email-not-found'; From 4449a071ecec0a5de8e7a142af1ba0dadd209115 Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Wed, 8 Aug 2018 18:52:51 +0300 Subject: [PATCH 04/11] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6865f1ac..d324eebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ way to update this template, but currently, we follow a pattern: --- ## Upcoming version 2018-08-XX +* [add] Listing availability + [#868](https://github.com/sharetribe/flex-template-web/pull/868), [#873](https://github.com/sharetribe/flex-template-web/pull/873), [#891](https://github.com/sharetribe/flex-template-web/pull/891) & [#892](https://github.com/sharetribe/flex-template-web/pull/892) + ## v1.3.2 2018-08-07 * [change] Update the Sharetribe Flex SDK to the 1.0.0 version in NPM. From 5e1aa7b244893795dde771675496f37e04606ef7 Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Fri, 10 Aug 2018 08:37:21 +0300 Subject: [PATCH 05/11] Add error handling for initiate request --- src/containers/CheckoutPage/CheckoutPage.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/containers/CheckoutPage/CheckoutPage.js b/src/containers/CheckoutPage/CheckoutPage.js index 7a3f91e6..a1d2597d 100644 --- a/src/containers/CheckoutPage/CheckoutPage.js +++ b/src/containers/CheckoutPage/CheckoutPage.js @@ -270,6 +270,10 @@ export class CheckoutPageComponent extends Component { ); const isAmountTooLowError = isTransactionInitiateAmountTooLowError(initiateOrderError); + const isBookingTimeNotAvailableError = isTransactionInitiateBookingTimeNotAvailableError( + initiateOrderError + ); + let initiateOrderErrorMessage = null; if (!listingNotFound && isAmountTooLowError) { @@ -278,6 +282,12 @@ export class CheckoutPageComponent extends Component {

); + } else if (!listingNotFound && isBookingTimeNotAvailableError) { + initiateOrderErrorMessage = ( +

+ +

+ ); } else if (!listingNotFound && initiateOrderError) { initiateOrderErrorMessage = (

From 19dd70c4334a065d4d7f16f0ac1e1a7fe0dcd278 Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Fri, 10 Aug 2018 08:55:47 +0300 Subject: [PATCH 06/11] Enable availability by default --- src/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index 3b94037a..7b219c62 100644 --- a/src/config.js +++ b/src/config.js @@ -38,7 +38,7 @@ const bookingUnitType = 'line-item/night'; // Should the application fetch available time slots (currently defined as // start and end dates) to be shown on listing page. -const fetchAvailableTimeSlots = false; +const fetchAvailableTimeSlots = true; // A maximum number of days forwards during which a booking can be made. // This is limited due to Stripe holding funds up to 90 days from the From b43a97f0db96f9f8ef041dbf7e23b5cef5ad6dca Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Fri, 10 Aug 2018 09:34:11 +0300 Subject: [PATCH 07/11] Remove an unused function --- .../FieldDateRangeInput/DateRangeInput.helpers.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/components/FieldDateRangeInput/DateRangeInput.helpers.js b/src/components/FieldDateRangeInput/DateRangeInput.helpers.js index 8d2d5f13..413ed0fd 100644 --- a/src/components/FieldDateRangeInput/DateRangeInput.helpers.js +++ b/src/components/FieldDateRangeInput/DateRangeInput.helpers.js @@ -28,16 +28,6 @@ const timeSlotsContain = (timeSlots, date) => { return timeSlots.findIndex(slot => timeSlotEqualsDay(slot, date)) > -1; }; -const lastBlockedBetweenExclusive = (timeSlots, startDate, endDate) => { - if (startDate.isSame(endDate, 'date')) { - return null; - } - - return timeSlotsContain(timeSlots, endDate) - ? lastBlockedBetweenExclusive(timeSlots, startDate, moment(endDate).subtract(1, 'days')) - : endDate; -}; - /** * Find first blocked date between two dates. * If none is found, null is returned. From 2c0cb83b5b6145a97d78441c9f626c1770634943 Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Fri, 10 Aug 2018 14:52:08 +0300 Subject: [PATCH 08/11] Add an example for day based bookings --- .../FieldDateRangeInput.example.js | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/components/FieldDateRangeInput/FieldDateRangeInput.example.js b/src/components/FieldDateRangeInput/FieldDateRangeInput.example.js index 699c0669..28a1b194 100644 --- a/src/components/FieldDateRangeInput/FieldDateRangeInput.example.js +++ b/src/components/FieldDateRangeInput/FieldDateRangeInput.example.js @@ -4,7 +4,7 @@ import { Form as FinalForm, FormSpy } from 'react-final-form'; import moment from 'moment'; import { Button } from '../../components'; import { required, bookingDatesRequired, composeValidators } from '../../util/validators'; -import { LINE_ITEM_NIGHT } from '../../util/types'; +import { LINE_ITEM_NIGHT, LINE_ITEM_DAY } from '../../util/types'; import { createTimeSlots } from '../../util/test-data'; import FieldDateRangeInput from './FieldDateRangeInput'; @@ -62,7 +62,7 @@ export const Empty = { dateInputProps: { name: 'bookingDates', unitType: LINE_ITEM_NIGHT, - startDateId: 'EmptyDateRangeInputForm.bookingStartDate', + startDateId: 'EmptyDateRange.bookingStartDate', startDateLabel: 'Start date', startDatePlaceholderText: moment().format('ddd, MMMM D'), endDateId: 'EmptyDateRangeInputForm.bookingEndDate', @@ -91,13 +91,50 @@ export const Empty = { group: 'custom inputs', }; -export const WithAvailableTimeSlots = { +export const WithAvailableTimeSlotsNighlyBooking = { component: FormComponent, props: { + style: { marginBottom: '140px' }, dateInputProps: { name: 'bookingDates', unitType: LINE_ITEM_NIGHT, - startDateId: 'WithAvailableTimeSlotsDateRangeInputForm.bookingStartDate', + startDateId: 'WithAvailableTimeSlotsDateRangeNightly.bookingStartDate', + startDateLabel: 'Start date', + startDatePlaceholderText: moment().format('ddd, MMMM D'), + endDateId: 'WithAvailableTimeSlotsDateRangeInputForm.bookingEndDate', + endDateLabel: 'End date', + endDatePlaceholderText: moment() + .add(1, 'days') + .format('ddd, MMMM D'), + format: null, + timeSlots: createAvailableTimeSlots(90, 60), + validate: composeValidators( + required('Required'), + bookingDatesRequired('Start date is not valid', 'End date is not valid') + ), + onBlur: () => console.log('onBlur called from DateRangeInput props.'), + onFocus: () => console.log('onFocus called from DateRangeInput props.'), + }, + onChange: formState => { + const { startDate, endDate } = formState.values; + if (startDate || endDate) { + console.log('Changed to', moment(startDate).format('L'), moment(endDate).format('L')); + } + }, + onSubmit: values => { + console.log('Submitting a form with values:', values); + }, + }, + group: 'custom inputs', +}; + +export const WithAvailableTimeSlotsDailyBooking = { + component: FormComponent, + props: { + dateInputProps: { + name: 'bookingDates', + unitType: LINE_ITEM_DAY, + startDateId: 'WithAvailableTimeSlotsDateRangeDaily.bookingStartDate', startDateLabel: 'Start date', startDatePlaceholderText: moment().format('ddd, MMMM D'), endDateId: 'WithAvailableTimeSlotsDateRangeInputForm.bookingEndDate', From e659ac480987affd55ffc43d82678ff4ecac1588 Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Mon, 13 Aug 2018 13:00:57 +0300 Subject: [PATCH 09/11] Fix a bug in daily booking Fixes a bug in daily booking where selected end date styles are not cleared when updating start date with a date that is before the old start date and a booked date can be found between those old and new start dates. --- .../DateRangeInput.helpers.js | 9 +--- .../FieldDateRangeInput/DateRangeInput.js | 48 +++++++++---------- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/src/components/FieldDateRangeInput/DateRangeInput.helpers.js b/src/components/FieldDateRangeInput/DateRangeInput.helpers.js index 413ed0fd..bfd4c4d3 100644 --- a/src/components/FieldDateRangeInput/DateRangeInput.helpers.js +++ b/src/components/FieldDateRangeInput/DateRangeInput.helpers.js @@ -132,14 +132,7 @@ export const isDayBlockedFn = (timeSlots, startDate, endDate, focusedInput) => { * Returns an isOutsideRange function that can be passed to * a react-dates DateRangePicker component. */ -export const isOutsideRangeFn = ( - timeSlots, - startDate, - endDate, - previousStartDate, - focusedInput, - unitType -) => { +export const isOutsideRangeFn = (timeSlots, startDate, endDate, focusedInput, unitType) => { const endOfRange = config.dayCountAvailableForBooking - 1; const lastBookableDate = moment().add(endOfRange, 'days'); diff --git a/src/components/FieldDateRangeInput/DateRangeInput.js b/src/components/FieldDateRangeInput/DateRangeInput.js index 27485f1f..666aaec7 100644 --- a/src/components/FieldDateRangeInput/DateRangeInput.js +++ b/src/components/FieldDateRangeInput/DateRangeInput.js @@ -123,7 +123,6 @@ class DateRangeInputComponent extends Component { this.state = { focusedInput: null, currentStartDate: null, - previousStartDate: null, }; this.blurTimeoutId = null; @@ -145,14 +144,29 @@ class DateRangeInputComponent extends Component { } onDatesChange(dates) { - const { unitType } = this.props; + const { unitType, timeSlots } = this.props; const { startDate, endDate } = dates; - const startDateAsDate = startDate instanceof moment ? startDate.toDate() : null; - const endDateAsDate = pickerEndDateToApiDate(unitType, endDate); - this.setState(prevState => ({ + // both dates are selected, a new start date before the previous start + // date is selected + const startDateUpdated = + timeSlots && + startDate && + endDate && + this.state.currentStartDate && + startDate.isBefore(this.state.currentStartDate); + + // clear the end date in case a blocked date can be found + // between previous start date and new start date + const clearEndDate = startDateUpdated + ? isBlockedBetween(timeSlots, startDate, moment(this.state.currentStartDate).add(1, 'days')) + : false; + + const startDateAsDate = startDate instanceof moment ? startDate.toDate() : null; + const endDateAsDate = clearEndDate ? null : pickerEndDateToApiDate(unitType, endDate); + + this.setState(() => ({ currentStartDate: startDateAsDate, - previousStartDate: prevState.currentStartDate, })); this.props.onChange({ startDate: startDateAsDate, endDate: endDateAsDate }); @@ -206,30 +220,12 @@ class DateRangeInputComponent extends Component { const endDate = apiEndDateToPickerDate(unitType, value ? value.endDate : null) || initialEndMoment; - // both dates are selected, a new start date before the previous start - // date is selected - const startDateUpdated = - timeSlots && - startDate && - endDate && - this.state.previousStartDate && - startDate.isBefore(this.state.previousStartDate); - - // clear the end date in case a blocked date can be found - // between previous start date and new start date - const clearEndDate = startDateUpdated - ? isBlockedBetween(timeSlots, startDate, moment(this.state.previousStartDate).add(1, 'days')) - : false; - - const endDateMaybe = clearEndDate ? null : endDate; - let isDayBlocked = isDayBlockedFn(timeSlots, startDate, endDate, this.state.focusedInput); let isOutsideRange = isOutsideRangeFn( timeSlots, startDate, - endDateMaybe, - this.state.previousStartDate, + endDate, this.state.focusedInput, unitType ); @@ -261,7 +257,7 @@ class DateRangeInputComponent extends Component { focusedInput={this.state.focusedInput} onFocusChange={this.onFocusChange} startDate={startDate} - endDate={endDateMaybe} + endDate={endDate} minimumNights={isDaily ? 0 : 1} onDatesChange={this.onDatesChange} startDatePlaceholderText={startDatePlaceholderTxt} From c282dc513d9959be6d00aca98c610819bd2d7f4d Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Mon, 13 Aug 2018 14:43:17 +0300 Subject: [PATCH 10/11] Fix selecting end date first Enable ending booking on the day another booking starts. --- .../DateRangeInput.helpers.js | 33 ++++++++++++++++--- .../FieldDateRangeInput/DateRangeInput.js | 8 ++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/components/FieldDateRangeInput/DateRangeInput.helpers.js b/src/components/FieldDateRangeInput/DateRangeInput.helpers.js index bfd4c4d3..c2fbc745 100644 --- a/src/components/FieldDateRangeInput/DateRangeInput.helpers.js +++ b/src/components/FieldDateRangeInput/DateRangeInput.helpers.js @@ -3,7 +3,7 @@ import { isSameDay, isInclusivelyAfterDay, isInclusivelyBeforeDay } from 'react- import { ensureTimeSlot } from '../../util/data'; import { START_DATE, END_DATE, dateFromAPIToLocalNoon } from '../../util/dates'; -import { LINE_ITEM_DAY, TIME_SLOT_DAY } from '../../util/types'; +import { LINE_ITEM_DAY, LINE_ITEM_NIGHT, TIME_SLOT_DAY } from '../../util/types'; import config from '../../config'; // Checks if time slot (propTypes.timeSlot) start time equals a day (moment) @@ -79,6 +79,9 @@ export const isBlockedBetween = (timeSlots, startDate, endDate) => export const isStartDateSelected = (timeSlots, startDate, endDate, focusedInput) => timeSlots && startDate && (!endDate || focusedInput === END_DATE) && focusedInput !== START_DATE; +export const isSelectingEndDateNightly = (timeSlots, startDate, endDate, focusedInput, unitType) => + timeSlots && !startDate && !endDate && focusedInput === END_DATE && unitType === LINE_ITEM_NIGHT; + export const apiEndDateToPickerDate = (unitType, endDate) => { const isValid = endDate instanceof Date; const isDaily = unitType === LINE_ITEM_DAY; @@ -112,20 +115,40 @@ export const pickerEndDateToApiDate = (unitType, endDate) => { * Returns an isDayBlocked function that can be passed to * a react-dates DateRangePicker component. */ -export const isDayBlockedFn = (timeSlots, startDate, endDate, focusedInput) => { +export const isDayBlockedFn = (timeSlots, startDate, endDate, focusedInput, unitType) => { const endOfRange = config.dayCountAvailableForBooking - 1; const lastBookableDate = moment().add(endOfRange, 'days'); // start date selected, end date missing const startDateSelected = isStartDateSelected(timeSlots, startDate, endDate, focusedInput); + // find the next booking after a start date const nextBookingStarts = startDateSelected ? firstBlockedBetween(timeSlots, startDate, moment(lastBookableDate).add(1, 'days')) : null; - return nextBookingStarts || !timeSlots - ? () => false - : day => !timeSlots.find(timeSlot => timeSlotEqualsDay(timeSlot, day)); + // end date is focused but no dates are selected + const selectingEndDate = isSelectingEndDateNightly( + timeSlots, + startDate, + endDate, + focusedInput, + unitType + ); + + if (selectingEndDate) { + // if end date is being selected first, block the day after a + // booked date as a booking can end on the day the following booking starts + return day => + !timeSlots.find(timeSlot => timeSlotEqualsDay(timeSlot, moment(day).subtract(1, 'days'))); + } else if (nextBookingStarts || !timeSlots) { + // a next booking is found or time slots are not provided + // -> booking range handles blocking dates + return () => false; + } else { + // otherwise return standard timeslots check + return day => !timeSlots.find(timeSlot => timeSlotEqualsDay(timeSlot, day)); + } }; /** diff --git a/src/components/FieldDateRangeInput/DateRangeInput.js b/src/components/FieldDateRangeInput/DateRangeInput.js index 666aaec7..c3541e32 100644 --- a/src/components/FieldDateRangeInput/DateRangeInput.js +++ b/src/components/FieldDateRangeInput/DateRangeInput.js @@ -220,7 +220,13 @@ class DateRangeInputComponent extends Component { const endDate = apiEndDateToPickerDate(unitType, value ? value.endDate : null) || initialEndMoment; - let isDayBlocked = isDayBlockedFn(timeSlots, startDate, endDate, this.state.focusedInput); + let isDayBlocked = isDayBlockedFn( + timeSlots, + startDate, + endDate, + this.state.focusedInput, + unitType + ); let isOutsideRange = isOutsideRangeFn( timeSlots, From cfd7494e2564c6a9a3d6a80fd6186db70b371218 Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Mon, 13 Aug 2018 15:16:31 +0300 Subject: [PATCH 11/11] Improve comments --- src/components/FieldDateRangeInput/DateRangeInput.helpers.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/FieldDateRangeInput/DateRangeInput.helpers.js b/src/components/FieldDateRangeInput/DateRangeInput.helpers.js index c2fbc745..1c540192 100644 --- a/src/components/FieldDateRangeInput/DateRangeInput.helpers.js +++ b/src/components/FieldDateRangeInput/DateRangeInput.helpers.js @@ -111,6 +111,7 @@ export const pickerEndDateToApiDate = (unitType, endDate) => { return endDate.toDate(); } }; + /** * Returns an isDayBlocked function that can be passed to * a react-dates DateRangePicker component. @@ -137,8 +138,8 @@ export const isDayBlockedFn = (timeSlots, startDate, endDate, focusedInput, unit ); if (selectingEndDate) { - // if end date is being selected first, block the day after a - // booked date as a booking can end on the day the following booking starts + // if end date is being selected first, block the day after a booked date + // (as a booking can end on the day the following booking starts) return day => !timeSlots.find(timeSlot => timeSlotEqualsDay(timeSlot, moment(day).subtract(1, 'days'))); } else if (nextBookingStarts || !timeSlots) {