flex-template-web/src/components/TransactionPanel/TransactionPanel.js

501 lines
17 KiB
JavaScript

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
import {
txIsAccepted,
txIsCanceled,
txIsDeclined,
txIsEnquired,
txIsPaymentExpired,
txIsPaymentPending,
txIsRequested,
txHasBeenDelivered,
} from '../../util/transaction';
import { LINE_ITEM_NIGHT, LINE_ITEM_DAY, propTypes } from '../../util/types';
import {
ensureListing,
ensureTransaction,
ensureUser,
userDisplayNameAsString,
} from '../../util/data';
import { isMobileSafari } from '../../util/userAgent';
import { formatMoney } from '../../util/currency';
import { AvatarLarge, BookingPanel, ReviewModal, UserDisplayName } from '../../components';
import { SendMessageForm } from '../../forms';
import config from '../../config';
// These are internal components that make this file more readable.
import AddressLinkMaybe from './AddressLinkMaybe';
import BreakdownMaybe from './BreakdownMaybe';
import DetailCardHeadingsMaybe from './DetailCardHeadingsMaybe';
import DetailCardImage from './DetailCardImage';
import FeedSection from './FeedSection';
import SaleActionButtonsMaybe from './SaleActionButtonsMaybe';
import PanelHeading, {
HEADING_ENQUIRED,
HEADING_PAYMENT_PENDING,
HEADING_PAYMENT_EXPIRED,
HEADING_REQUESTED,
HEADING_ACCEPTED,
HEADING_DECLINED,
HEADING_CANCELED,
HEADING_DELIVERED,
} from './PanelHeading';
import css from './TransactionPanel.css';
// Helper function to get display names for different roles
const displayNames = (currentUser, currentProvider, currentCustomer, intl) => {
const authorDisplayName = <UserDisplayName user={currentProvider} intl={intl} />;
const customerDisplayName = <UserDisplayName user={currentCustomer} intl={intl} />;
let otherUserDisplayName = '';
let otherUserDisplayNameString = '';
const currentUserIsCustomer =
currentUser.id && currentCustomer.id && currentUser.id.uuid === currentCustomer.id.uuid;
const currentUserIsProvider =
currentUser.id && currentProvider.id && currentUser.id.uuid === currentProvider.id.uuid;
if (currentUserIsCustomer) {
otherUserDisplayName = authorDisplayName;
otherUserDisplayNameString = userDisplayNameAsString(currentProvider, '');
} else if (currentUserIsProvider) {
otherUserDisplayName = customerDisplayName;
otherUserDisplayNameString = userDisplayNameAsString(currentCustomer, '');
}
return {
authorDisplayName,
customerDisplayName,
otherUserDisplayName,
otherUserDisplayNameString,
};
};
export class TransactionPanelComponent extends Component {
constructor(props) {
super(props);
this.state = {
sendMessageFormFocused: false,
isReviewModalOpen: false,
reviewSubmitted: false,
};
this.isMobSaf = false;
this.sendMessageFormName = 'TransactionPanel.SendMessageForm';
this.onOpenReviewModal = this.onOpenReviewModal.bind(this);
this.onSubmitReview = this.onSubmitReview.bind(this);
this.onSendMessageFormFocus = this.onSendMessageFormFocus.bind(this);
this.onSendMessageFormBlur = this.onSendMessageFormBlur.bind(this);
this.onMessageSubmit = this.onMessageSubmit.bind(this);
this.scrollToMessage = this.scrollToMessage.bind(this);
}
componentWillMount() {
this.isMobSaf = isMobileSafari();
}
onOpenReviewModal() {
this.setState({ isReviewModalOpen: true });
}
onSubmitReview(values) {
const { onSendReview, transaction, transactionRole } = this.props;
const currentTransaction = ensureTransaction(transaction);
const { reviewRating, reviewContent } = values;
const rating = Number.parseInt(reviewRating, 10);
onSendReview(transactionRole, currentTransaction, rating, reviewContent)
.then(r => this.setState({ isReviewModalOpen: false, reviewSubmitted: true }))
.catch(e => {
// Do nothing.
});
}
onSendMessageFormFocus() {
this.setState({ sendMessageFormFocused: true });
if (this.isMobSaf) {
// Scroll to bottom
window.scroll({ top: document.body.scrollHeight, left: 0, behavior: 'smooth' });
}
}
onSendMessageFormBlur() {
this.setState({ sendMessageFormFocused: false });
}
onMessageSubmit(values, form) {
const message = values.message ? values.message.trim() : null;
const { transaction, onSendMessage } = this.props;
const ensuredTransaction = ensureTransaction(transaction);
if (!message) {
return;
}
onSendMessage(ensuredTransaction.id, message)
.then(messageId => {
form.reset();
this.scrollToMessage(messageId);
})
.catch(e => {
// Ignore, Redux handles the error
});
}
scrollToMessage(messageId) {
const selector = `#msg-${messageId.uuid}`;
const el = document.querySelector(selector);
if (el) {
el.scrollIntoView({
block: 'start',
behavior: 'smooth',
});
}
}
render() {
const {
rootClassName,
className,
currentUser,
transaction,
totalMessagePages,
oldestMessagePageFetched,
messages,
initialMessageFailed,
fetchMessagesInProgress,
fetchMessagesError,
sendMessageInProgress,
sendMessageError,
sendReviewInProgress,
sendReviewError,
onManageDisableScrolling,
onShowMoreMessages,
transactionRole,
intl,
onAcceptSale,
onDeclineSale,
acceptInProgress,
declineInProgress,
acceptSaleError,
declineSaleError,
onSubmitBookingRequest,
timeSlots,
fetchTimeSlotsError,
} = this.props;
const currentTransaction = ensureTransaction(transaction);
const currentListing = ensureListing(currentTransaction.listing);
const currentProvider = ensureUser(currentTransaction.provider);
const currentCustomer = ensureUser(currentTransaction.customer);
const isCustomer = transactionRole === 'customer';
const isProvider = transactionRole === 'provider';
const listingLoaded = !!currentListing.id;
const listingDeleted = listingLoaded && currentListing.attributes.deleted;
const iscustomerLoaded = !!currentCustomer.id;
const isCustomerBanned = iscustomerLoaded && currentCustomer.attributes.banned;
const isCustomerDeleted = iscustomerLoaded && currentCustomer.attributes.deleted;
const isProviderLoaded = !!currentProvider.id;
const isProviderBanned = isProviderLoaded && currentProvider.attributes.banned;
const isProviderDeleted = isProviderLoaded && currentProvider.attributes.deleted;
const stateDataFn = tx => {
if (txIsEnquired(tx)) {
return {
headingState: HEADING_ENQUIRED,
showBookingPanel: isCustomer && !isProviderBanned,
};
} else if (txIsPaymentPending(tx)) {
return {
headingState: HEADING_PAYMENT_PENDING,
showDetailCardHeadings: isCustomer,
};
} else if (txIsPaymentExpired(tx)) {
return {
headingState: HEADING_PAYMENT_EXPIRED,
showDetailCardHeadings: isCustomer,
};
} else if (txIsRequested(tx)) {
return {
headingState: HEADING_REQUESTED,
showDetailCardHeadings: isCustomer,
showSaleButtons: isProvider && !isCustomerBanned,
};
} else if (txIsAccepted(tx)) {
return {
headingState: HEADING_ACCEPTED,
showDetailCardHeadings: isCustomer,
showAddress: isCustomer,
};
} else if (txIsDeclined(tx)) {
return {
headingState: HEADING_DECLINED,
showDetailCardHeadings: isCustomer,
};
} else if (txIsCanceled(tx)) {
return {
headingState: HEADING_CANCELED,
showDetailCardHeadings: isCustomer,
};
} else if (txHasBeenDelivered(tx)) {
return {
headingState: HEADING_DELIVERED,
showDetailCardHeadings: isCustomer,
showAddress: isCustomer,
};
} else {
return { headingState: 'unknown' };
}
};
const stateData = stateDataFn(currentTransaction);
const deletedListingTitle = intl.formatMessage({
id: 'TransactionPanel.deletedListingTitle',
});
const {
authorDisplayName,
customerDisplayName,
otherUserDisplayName,
otherUserDisplayNameString,
} = displayNames(currentUser, currentProvider, currentCustomer, intl);
const { publicData, geolocation } = currentListing.attributes;
const location = publicData && publicData.location ? publicData.location : {};
const listingTitle = currentListing.attributes.deleted
? deletedListingTitle
: currentListing.attributes.title;
const unitType = config.bookingUnitType;
const isNightly = unitType === LINE_ITEM_NIGHT;
const isDaily = unitType === LINE_ITEM_DAY;
const unitTranslationKey = isNightly
? 'TransactionPanel.perNight'
: isDaily
? 'TransactionPanel.perDay'
: 'TransactionPanel.perUnit';
const price = currentListing.attributes.price;
const bookingSubTitle = price
? `${formatMoney(intl, price)} ${intl.formatMessage({ id: unitTranslationKey })}`
: '';
const firstImage =
currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
const saleButtons = (
<SaleActionButtonsMaybe
showButtons={stateData.showSaleButtons}
acceptInProgress={acceptInProgress}
declineInProgress={declineInProgress}
acceptSaleError={acceptSaleError}
declineSaleError={declineSaleError}
onAcceptSale={() => onAcceptSale(currentTransaction.id)}
onDeclineSale={() => onDeclineSale(currentTransaction.id)}
/>
);
const showSendMessageForm =
!isCustomerBanned && !isCustomerDeleted && !isProviderBanned && !isProviderDeleted;
const sendMessagePlaceholder = intl.formatMessage(
{ id: 'TransactionPanel.sendMessagePlaceholder' },
{ name: otherUserDisplayNameString }
);
const sendingMessageNotAllowed = intl.formatMessage({
id: 'TransactionPanel.sendingMessageNotAllowed',
});
const classes = classNames(rootClassName || css.root, className);
return (
<div className={classes}>
<div className={css.container}>
<div className={css.txInfo}>
<DetailCardImage
rootClassName={css.imageWrapperMobile}
avatarWrapperClassName={css.avatarWrapperMobile}
listingTitle={listingTitle}
image={firstImage}
provider={currentProvider}
isCustomer={isCustomer}
/>
{isProvider ? (
<div className={css.avatarWrapperProviderDesktop}>
<AvatarLarge user={currentCustomer} className={css.avatarDesktop} />
</div>
) : null}
<PanelHeading
panelHeadingState={stateData.headingState}
transactionRole={transactionRole}
providerName={authorDisplayName}
customerName={customerDisplayName}
isCustomerBanned={isCustomerBanned}
listingId={currentListing.id && currentListing.id.uuid}
listingTitle={listingTitle}
listingDeleted={listingDeleted}
/>
<div className={css.bookingDetailsMobile}>
<AddressLinkMaybe
rootClassName={css.addressMobile}
location={location}
geolocation={geolocation}
showAddress={stateData.showAddress}
/>
<BreakdownMaybe transaction={currentTransaction} transactionRole={transactionRole} />
</div>
<FeedSection
rootClassName={css.feedContainer}
currentTransaction={currentTransaction}
currentUser={currentUser}
fetchMessagesError={fetchMessagesError}
fetchMessagesInProgress={fetchMessagesInProgress}
initialMessageFailed={initialMessageFailed}
messages={messages}
oldestMessagePageFetched={oldestMessagePageFetched}
onOpenReviewModal={this.onOpenReviewModal}
onShowMoreMessages={() => onShowMoreMessages(currentTransaction.id)}
totalMessagePages={totalMessagePages}
/>
{showSendMessageForm ? (
<SendMessageForm
form={this.sendMessageFormName}
rootClassName={css.sendMessageForm}
messagePlaceholder={sendMessagePlaceholder}
inProgress={sendMessageInProgress}
sendMessageError={sendMessageError}
onFocus={this.onSendMessageFormFocus}
onBlur={this.onSendMessageFormBlur}
onSubmit={this.onMessageSubmit}
/>
) : (
<div className={css.sendingMessageNotAllowed}>{sendingMessageNotAllowed}</div>
)}
{stateData.showSaleButtons ? (
<div className={css.mobileActionButtons}>{saleButtons}</div>
) : null}
</div>
<div className={css.asideDesktop}>
<div className={css.detailCard}>
<DetailCardImage
avatarWrapperClassName={css.avatarWrapperDesktop}
listingTitle={listingTitle}
image={firstImage}
provider={currentProvider}
isCustomer={isCustomer}
/>
<DetailCardHeadingsMaybe
showDetailCardHeadings={stateData.showDetailCardHeadings}
listingTitle={listingTitle}
subTitle={bookingSubTitle}
location={location}
geolocation={geolocation}
showAddress={stateData.showAddress}
/>
{stateData.showBookingPanel ? (
<BookingPanel
className={css.bookingPanel}
titleClassName={css.bookingTitle}
isOwnListing={false}
listing={currentListing}
title={listingTitle}
subTitle={bookingSubTitle}
authorDisplayName={authorDisplayName}
onSubmit={onSubmitBookingRequest}
onManageDisableScrolling={onManageDisableScrolling}
timeSlots={timeSlots}
fetchTimeSlotsError={fetchTimeSlotsError}
/>
) : null}
<BreakdownMaybe
className={css.breakdownContainer}
transaction={currentTransaction}
transactionRole={transactionRole}
/>
{stateData.showSaleButtons ? (
<div className={css.desktopActionButtons}>{saleButtons}</div>
) : null}
</div>
</div>
</div>
<ReviewModal
id="ReviewOrderModal"
isOpen={this.state.isReviewModalOpen}
onCloseModal={() => this.setState({ isReviewModalOpen: false })}
onManageDisableScrolling={onManageDisableScrolling}
onSubmitReview={this.onSubmitReview}
revieweeName={otherUserDisplayName}
reviewSent={this.state.reviewSubmitted}
sendReviewInProgress={sendReviewInProgress}
sendReviewError={sendReviewError}
/>
</div>
);
}
}
TransactionPanelComponent.defaultProps = {
rootClassName: null,
className: null,
currentUser: null,
acceptSaleError: null,
declineSaleError: null,
fetchMessagesError: null,
initialMessageFailed: null,
sendMessageError: null,
sendReviewError: null,
timeSlots: null,
fetchTimeSlotsError: null,
};
const { arrayOf, bool, func, number, string } = PropTypes;
TransactionPanelComponent.propTypes = {
rootClassName: string,
className: string,
currentUser: propTypes.currentUser,
transaction: propTypes.transaction.isRequired,
totalMessagePages: number.isRequired,
oldestMessagePageFetched: number.isRequired,
messages: arrayOf(propTypes.message).isRequired,
initialMessageFailed: bool,
fetchMessagesInProgress: bool.isRequired,
fetchMessagesError: propTypes.error,
sendMessageInProgress: bool.isRequired,
sendMessageError: propTypes.error,
sendReviewInProgress: bool.isRequired,
sendReviewError: propTypes.error,
onManageDisableScrolling: func.isRequired,
onShowMoreMessages: func.isRequired,
onSendMessage: func.isRequired,
onSendReview: func.isRequired,
onSubmitBookingRequest: func.isRequired,
timeSlots: arrayOf(propTypes.timeSlot),
fetchTimeSlotsError: propTypes.error,
// Sale related props
onAcceptSale: func.isRequired,
onDeclineSale: func.isRequired,
acceptInProgress: bool.isRequired,
declineInProgress: bool.isRequired,
acceptSaleError: propTypes.error,
declineSaleError: propTypes.error,
// from injectIntl
intl: intlShape,
};
const TransactionPanel = injectIntl(TransactionPanelComponent);
export default TransactionPanel;