mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
501 lines
17 KiB
JavaScript
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;
|