|
| 1 | +import React, {useMemo} from 'react'; |
| 2 | +import type {StyleProp, ViewStyle} from 'react-native'; |
| 3 | +import {FlatList, View} from 'react-native'; |
| 4 | +import {useOnyx} from 'react-native-onyx'; |
| 5 | +import Button from '@components/Button'; |
| 6 | +import Icon from '@components/Icon'; |
| 7 | +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; |
| 8 | +import OfflineWithFeedback from '@components/OfflineWithFeedback'; |
| 9 | +import {PressableWithoutFeedback} from '@components/Pressable'; |
| 10 | +import {showContextMenuForReport} from '@components/ShowContextMenuContext'; |
| 11 | +import Text from '@components/Text'; |
| 12 | +import useLocalize from '@hooks/useLocalize'; |
| 13 | +import useStyleUtils from '@hooks/useStyleUtils'; |
| 14 | +import useTheme from '@hooks/useTheme'; |
| 15 | +import useThemeStyles from '@hooks/useThemeStyles'; |
| 16 | +import ControlSelection from '@libs/ControlSelection'; |
| 17 | +import * as CurrencyUtils from '@libs/CurrencyUtils'; |
| 18 | +import DateUtils from '@libs/DateUtils'; |
| 19 | +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; |
| 20 | +import Navigation from '@libs/Navigation/Navigation'; |
| 21 | +import * as ReportUtils from '@libs/ReportUtils'; |
| 22 | +import * as TripReservationUtils from '@libs/TripReservationUtils'; |
| 23 | +import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; |
| 24 | +import variables from '@styles/variables'; |
| 25 | +import * as Expensicons from '@src/components/Icon/Expensicons'; |
| 26 | +import CONST from '@src/CONST'; |
| 27 | +import ONYXKEYS from '@src/ONYXKEYS'; |
| 28 | +import ROUTES from '@src/ROUTES'; |
| 29 | +import type {ReportAction} from '@src/types/onyx'; |
| 30 | +import type {Reservation} from '@src/types/onyx/Transaction'; |
| 31 | + |
| 32 | +type TripRoomPreviewProps = { |
| 33 | + /** All the data of the action */ |
| 34 | + action: ReportAction; |
| 35 | + |
| 36 | + /** The associated chatReport */ |
| 37 | + chatReportID: string; |
| 38 | + |
| 39 | + /** Extra styles to pass to View wrapper */ |
| 40 | + containerStyles?: StyleProp<ViewStyle>; |
| 41 | + |
| 42 | + /** Popover context menu anchor, used for showing context menu */ |
| 43 | + contextMenuAnchor?: ContextMenuAnchor; |
| 44 | + |
| 45 | + /** Callback for updating context menu active state, used for showing context menu */ |
| 46 | + checkIfContextMenuActive?: () => void; |
| 47 | + |
| 48 | + /** Whether the corresponding report action item is hovered */ |
| 49 | + isHovered?: boolean; |
| 50 | +}; |
| 51 | + |
| 52 | +type ReservationViewProps = { |
| 53 | + reservation: Reservation; |
| 54 | +}; |
| 55 | + |
| 56 | +function ReservationView({reservation}: ReservationViewProps) { |
| 57 | + const theme = useTheme(); |
| 58 | + const styles = useThemeStyles(); |
| 59 | + const StyleUtils = useStyleUtils(); |
| 60 | + const {translate} = useLocalize(); |
| 61 | + |
| 62 | + const reservationIcon = TripReservationUtils.getTripReservationIcon(reservation.type); |
| 63 | + const title = reservation.type === CONST.RESERVATION_TYPE.CAR ? reservation.carInfo?.name : reservation.start.longName; |
| 64 | + |
| 65 | + const titleComponent = |
| 66 | + reservation.type === CONST.RESERVATION_TYPE.FLIGHT ? ( |
| 67 | + <View style={[styles.flexRow, styles.alignItemsCenter, styles.gap2]}> |
| 68 | + <Text style={styles.labelStrong}>{reservation.start.shortName}</Text> |
| 69 | + <Icon |
| 70 | + src={Expensicons.ArrowRightLong} |
| 71 | + width={variables.iconSizeSmall} |
| 72 | + height={variables.iconSizeSmall} |
| 73 | + fill={theme.icon} |
| 74 | + /> |
| 75 | + <Text style={styles.labelStrong}>{reservation.end.shortName}</Text> |
| 76 | + </View> |
| 77 | + ) : ( |
| 78 | + <Text |
| 79 | + numberOfLines={1} |
| 80 | + style={styles.labelStrong} |
| 81 | + > |
| 82 | + {title} |
| 83 | + </Text> |
| 84 | + ); |
| 85 | + |
| 86 | + return ( |
| 87 | + <MenuItemWithTopDescription |
| 88 | + description={translate(`travel.${reservation.type}`)} |
| 89 | + descriptionTextStyle={styles.textMicro} |
| 90 | + titleComponent={titleComponent} |
| 91 | + titleContainerStyle={styles.gap1} |
| 92 | + secondaryIcon={reservationIcon} |
| 93 | + secondaryIconFill={theme.icon} |
| 94 | + wrapperStyle={[styles.taskDescriptionMenuItem, styles.p0]} |
| 95 | + shouldGreyOutWhenDisabled={false} |
| 96 | + numberOfLinesTitle={0} |
| 97 | + interactive={false} |
| 98 | + iconHeight={variables.iconSizeSmall} |
| 99 | + iconWidth={variables.iconSizeSmall} |
| 100 | + iconStyles={[StyleUtils.getTripReservationIconContainer(true), styles.mr3]} |
| 101 | + isSmallAvatarSubscriptMenu |
| 102 | + /> |
| 103 | + ); |
| 104 | +} |
| 105 | + |
| 106 | +const renderItem = ({item}: {item: Reservation}) => <ReservationView reservation={item} />; |
| 107 | + |
| 108 | +function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnchor, isHovered = false, checkIfContextMenuActive = () => {}}: TripRoomPreviewProps) { |
| 109 | + const styles = useThemeStyles(); |
| 110 | + const {translate} = useLocalize(); |
| 111 | + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); |
| 112 | + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`); |
| 113 | + |
| 114 | + const tripTransactions = ReportUtils.getTripTransactions(chatReport?.iouReportID, 'reportID'); |
| 115 | + const reservations: Reservation[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); |
| 116 | + const dateInfo = chatReport?.tripData ? DateUtils.getFormattedDateRange(new Date(chatReport.tripData.startDate), new Date(chatReport.tripData.endDate)) : ''; |
| 117 | + const {totalDisplaySpend} = ReportUtils.getMoneyRequestSpendBreakdown(chatReport); |
| 118 | + |
| 119 | + const displayAmount = useMemo(() => { |
| 120 | + if (totalDisplaySpend) { |
| 121 | + return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency); |
| 122 | + } |
| 123 | + |
| 124 | + // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") |
| 125 | + let displayAmountValue = ''; |
| 126 | + const actionMessage = action.message?.[0]?.text ?? ''; |
| 127 | + const splits = actionMessage.split(' '); |
| 128 | + |
| 129 | + splits.forEach((split) => { |
| 130 | + if (!/\d/.test(split)) { |
| 131 | + return; |
| 132 | + } |
| 133 | + |
| 134 | + displayAmountValue = split; |
| 135 | + }); |
| 136 | + |
| 137 | + return displayAmountValue; |
| 138 | + }, [action.message, iouReport?.currency, totalDisplaySpend]); |
| 139 | + |
| 140 | + return ( |
| 141 | + <OfflineWithFeedback |
| 142 | + pendingAction={action?.pendingAction} |
| 143 | + shouldDisableOpacity={!!(action.pendingAction ?? action.isOptimisticAction)} |
| 144 | + needsOffscreenAlphaCompositing |
| 145 | + > |
| 146 | + <View style={[styles.chatItemMessage, containerStyles]}> |
| 147 | + <PressableWithoutFeedback |
| 148 | + onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} |
| 149 | + onPressOut={() => ControlSelection.unblock()} |
| 150 | + onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)} |
| 151 | + shouldUseHapticsOnLongPress |
| 152 | + style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox, styles.cursorDefault]} |
| 153 | + role={CONST.ROLE.BUTTON} |
| 154 | + accessibilityLabel={translate('iou.viewDetails')} |
| 155 | + > |
| 156 | + <View style={[styles.moneyRequestPreviewBox, styles.p4, styles.gap5, isHovered ? styles.reportPreviewBoxHoverBorder : undefined]}> |
| 157 | + <View style={styles.expenseAndReportPreviewTextContainer}> |
| 158 | + <View style={styles.reportPreviewAmountSubtitleContainer}> |
| 159 | + <View style={styles.flexRow}> |
| 160 | + <View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}> |
| 161 | + <Text style={[styles.textLabelSupporting, styles.lh16]}> |
| 162 | + {translate('travel.trip')} • {dateInfo} |
| 163 | + </Text> |
| 164 | + </View> |
| 165 | + </View> |
| 166 | + </View> |
| 167 | + <View style={styles.reportPreviewAmountSubtitleContainer}> |
| 168 | + <View style={styles.flexRow}> |
| 169 | + <View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}> |
| 170 | + <Text style={styles.textHeadlineH2}>{displayAmount}</Text> |
| 171 | + </View> |
| 172 | + </View> |
| 173 | + <View style={styles.flexRow}> |
| 174 | + <View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}> |
| 175 | + <Text style={[styles.textLabelSupporting, styles.textNormal, styles.lh20]}>{chatReport?.reportName}</Text> |
| 176 | + </View> |
| 177 | + </View> |
| 178 | + </View> |
| 179 | + </View> |
| 180 | + <FlatList |
| 181 | + data={reservations} |
| 182 | + style={styles.gap3} |
| 183 | + renderItem={renderItem} |
| 184 | + /> |
| 185 | + <Button |
| 186 | + medium |
| 187 | + success |
| 188 | + text={translate('travel.viewTrip')} |
| 189 | + onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chatReportID))} |
| 190 | + /> |
| 191 | + </View> |
| 192 | + </PressableWithoutFeedback> |
| 193 | + </View> |
| 194 | + </OfflineWithFeedback> |
| 195 | + ); |
| 196 | +} |
| 197 | + |
| 198 | +TripRoomPreview.displayName = 'TripRoomPreview'; |
| 199 | + |
| 200 | +export default TripRoomPreview; |
0 commit comments