Skip to content

Commit

Permalink
Merge branch 'main' into track-expense
Browse files Browse the repository at this point in the history
  • Loading branch information
shubham1206agra authored Mar 20, 2024
2 parents e0b531f + e1d7718 commit c5ac594
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 67 deletions.
41 changes: 41 additions & 0 deletions docs/articles/expensify-classic/expenses/Track-group-expenses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: Track group expenses
description: Use Attendee Tracking to track group expenses
---
<div id="expensify-classic" markdown="1">

Capture group and event expenses with Attendee Tracking by documenting who attended and the cost per attendee. The amount is always divided evenly between all attendees—different amounts cannot be allocated to specific attendees. To divide the amounts differently, you’ll first have to split the expense.

{% include info.html %}
Attendees added to an expense will not be notified that they were added to an expense, nor will they share in the expense or be requested to pay for any portion of the expense.
{% include end-info.html %}

{% include selector.html values="desktop, mobile" %}

{% include option.html value="desktop" %}
1. Click the **Expenses** tab.
2. Click the expense you want to add attendees to.
3. Click the attendees field and enter the name or email address of the attendee.
- If the attendee is a member of your workspace, you can select their name from the list.
- If the attendee is not a member of your workspace, enter their full name or email address and press Enter on your keyboard to add them as a new attendee.
4. Click **Save**.

Once added, you’ll also see the list of attendees in the expense overview on the Expenses tab. To see the cost per employee, hover over the receipt total. These details are also available on any report that you add the expense to.
{% include end-option.html %}

{% include option.html value="mobile" %}
1. Tap the **Expenses** tab.
2. Tap the expense you want to add attendees to.
3. Scroll down to the bottom and tap **More Options**.
4. Tap the attendees field and enter the name or email address of the attendee.
- If the attendee is a member of your workspace, you can select their name from the list.
- If the attendee is not a member of your workspace, enter their full name or email address and press Enter on your keyboard to add them as a new attendee.
5. Tap **Save**.

Attendees will also be listed on any report that you add the expense to.

{% include end-option.html %}

{% include end-selector.html %}

</div>
2 changes: 1 addition & 1 deletion src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const ROUTES = {
},
PROFILE_AVATAR: {
route: 'a/:accountID/avatar',
getRoute: (accountID: string) => `a/${accountID}/avatar` as const,
getRoute: (accountID: string | number) => `a/${accountID}/avatar` as const,
},

TRANSITION_BETWEEN_APPS: 'transition',
Expand Down
3 changes: 2 additions & 1 deletion src/components/ReportActionItem/ReportPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ function ReportPreview({

const hasReceipts = transactionsWithReceipts.length > 0;
const isScanning = hasReceipts && areAllRequestsBeingSmartScanned;
const hasErrors = hasMissingSmartscanFields || (canUseViolations && ReportUtils.hasViolations(iouReportID, transactionViolations));
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const hasErrors = hasMissingSmartscanFields || (canUseViolations && ReportUtils.hasViolations(iouReportID, transactionViolations)) || ReportUtils.hasActionsWithErrors(iouReportID);
const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3);
const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,63 +1,60 @@
import PropTypes from 'prop-types';
import React, {memo} from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Icon} from '@src/types/onyx/OnyxCommon';
import Avatar from './Avatar';
import avatarPropTypes from './avatarPropTypes';
import PressableWithoutFocus from './Pressable/PressableWithoutFocus';
import Text from './Text';

const propTypes = {
icons: PropTypes.arrayOf(avatarPropTypes),
reportID: PropTypes.string,
type RoomHeaderAvatarsProps = {
icons: Icon[];
reportID: string;
};

const defaultProps = {
icons: [],
reportID: '',
};

function RoomHeaderAvatars(props) {
const navigateToAvatarPage = (icon) => {
function RoomHeaderAvatars({icons, reportID}: RoomHeaderAvatarsProps) {
const navigateToAvatarPage = (icon: Icon) => {
if (icon.type === CONST.ICON_TYPE_WORKSPACE) {
Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(props.reportID));
Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID));
return;
}
Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(icon.id));

if (icon.id) {
Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(icon.id));
}
};

const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
if (!props.icons.length) {

if (!icons.length) {
return null;
}

if (props.icons.length === 1) {
if (icons.length === 1) {
return (
<PressableWithoutFocus
style={[styles.noOutline]}
onPress={() => navigateToAvatarPage(props.icons[0])}
style={styles.noOutline}
onPress={() => navigateToAvatarPage(icons[0])}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
accessibilityLabel={props.icons[0].name}
accessibilityLabel={icons[0].name ?? ''}
>
<Avatar
source={props.icons[0].source}
imageStyles={[styles.avatarLarge]}
source={icons[0].source}
imageStyles={styles.avatarLarge}
size={CONST.AVATAR_SIZE.LARGE}
name={props.icons[0].name}
type={props.icons[0].type}
fallbackIcon={props.icons[0].fallbackIcon}
name={icons[0].name}
type={icons[0].type}
fallbackIcon={icons[0].fallbackIcon}
/>
</PressableWithoutFocus>
);
}

const iconsToDisplay = props.icons.slice(0, CONST.REPORT.MAX_PREVIEW_AVATARS);
const iconsToDisplay = icons.slice(0, CONST.REPORT.MAX_PREVIEW_AVATARS);

const iconStyle = [
styles.roomHeaderAvatar,
Expand All @@ -68,16 +65,17 @@ function RoomHeaderAvatars(props) {
return (
<View style={styles.pointerEventsBoxNone}>
<View style={[styles.flexRow, styles.wAuto, styles.ml3]}>
{_.map(iconsToDisplay, (icon, index) => (
{iconsToDisplay.map((icon, index) => (
<View
// eslint-disable-next-line react/no-array-index-key
key={`${icon.id}${index}`}
style={[styles.justifyContentCenter, styles.alignItemsCenter]}
>
<PressableWithoutFocus
style={[styles.mln4, StyleUtils.getAvatarBorderRadius(CONST.AVATAR_SIZE.LARGE_BORDERED, icon.type)]}
onPress={() => navigateToAvatarPage(icon)}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
accessibilityLabel={icon.name}
accessibilityLabel={icon.name ?? ''}
>
<Avatar
source={icon.source}
Expand All @@ -88,7 +86,7 @@ function RoomHeaderAvatars(props) {
fallbackIcon={icon.fallbackIcon}
/>
</PressableWithoutFocus>
{index === CONST.REPORT.MAX_PREVIEW_AVATARS - 1 && props.icons.length - CONST.REPORT.MAX_PREVIEW_AVATARS !== 0 && (
{index === CONST.REPORT.MAX_PREVIEW_AVATARS - 1 && icons.length - CONST.REPORT.MAX_PREVIEW_AVATARS !== 0 && (
<>
<View
style={[
Expand All @@ -100,7 +98,7 @@ function RoomHeaderAvatars(props) {
styles.roomHeaderAvatarOverlay,
]}
/>
<Text style={styles.avatarInnerTextChat}>{`+${props.icons.length - CONST.REPORT.MAX_PREVIEW_AVATARS}`}</Text>
<Text style={styles.avatarInnerTextChat}>{`+${icons.length - CONST.REPORT.MAX_PREVIEW_AVATARS}`}</Text>
</>
)}
</View>
Expand All @@ -110,8 +108,6 @@ function RoomHeaderAvatars(props) {
);
}

RoomHeaderAvatars.defaultProps = defaultProps;
RoomHeaderAvatars.propTypes = propTypes;
RoomHeaderAvatars.displayName = 'RoomHeaderAvatars';

export default memo(RoomHeaderAvatars);
4 changes: 3 additions & 1 deletion src/components/VideoPlayer/BaseVideoPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import CONST from '@src/CONST';
import {videoPlayerDefaultProps, videoPlayerPropTypes} from './propTypes';
import shouldReplayVideo from './shouldReplayVideo';
import * as VideoUtils from './utils';
import VideoPlayerControls from './VideoPlayerControls';

const isMobileSafari = Browser.isMobileSafari();
Expand Down Expand Up @@ -49,7 +50,8 @@ function BaseVideoPlayer({
const [isPlaying, setIsPlaying] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isBuffering, setIsBuffering] = useState(true);
const [sourceURL] = useState(url.includes('blob:') || url.includes('file:///') ? url : addEncryptedAuthTokenToURL(url));
// we add "#t=0.001" at the end of the URL to skip first milisecond of the video and always be able to show proper video preview when video is paused at the beginning
const [sourceURL] = useState(VideoUtils.addSkipTimeTagToURL(url.includes('blob:') || url.includes('file:///') ? url : addEncryptedAuthTokenToURL(url), 0.001));
const [isPopoverVisible, setIsPopoverVisible] = useState(false);
const [popoverAnchorPosition, setPopoverAnchorPosition] = useState({horizontal: 0, vertical: 0});
const videoPlayerRef = useRef(null);
Expand Down
2 changes: 1 addition & 1 deletion src/components/VideoPlayer/VideoPlayerControls/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import refPropTypes from '@components/refPropTypes';
import Text from '@components/Text';
import IconButton from '@components/VideoPlayer/IconButton';
import convertMillisecondsToTime from '@components/VideoPlayer/utils';
import {convertMillisecondsToTime} from '@components/VideoPlayer/utils';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down
20 changes: 16 additions & 4 deletions src/components/VideoPlayer/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
// Converts milliseconds to '[hours:]minutes:seconds' format
const convertMillisecondsToTime = (milliseconds: number) => {
/**
* Converts milliseconds to '[hours:]minutes:seconds' format
*/
function convertMillisecondsToTime(milliseconds: number) {
const hours = Math.floor(milliseconds / 3600000);
const minutes = Math.floor((milliseconds / 60000) % 60);
const seconds = Math.floor((milliseconds / 1000) % 60)
.toFixed(0)
.padStart(2, '0');
return hours > 0 ? `${hours}:${String(minutes).padStart(2, '0')}:${seconds}` : `${minutes}:${seconds}`;
};
}

export default convertMillisecondsToTime;
/**
* Adds a #t=seconds tag to the end of the URL to skip first seconds of the video
*/
function addSkipTimeTagToURL(url: string, seconds: number) {
if (url.includes('#t=')) {
return url;
}
return `${url}#t=${seconds}`;
}

export {convertMillisecondsToTime, addSkipTimeTagToURL};
4 changes: 2 additions & 2 deletions src/libs/NextStepUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ function buildNextStep(
text: 'Waiting for ',
},
{
text: managerDisplayName,
text: 'you',
type: 'strong',
},
{
Expand All @@ -281,7 +281,7 @@ function buildNextStep(
text: 'Waiting for ',
},
{
text: 'you',
text: managerDisplayName,
type: 'strong',
},
{
Expand Down
9 changes: 9 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5415,6 +5415,14 @@ function shouldCreateNewMoneyRequestReport(existingIOUReport: OnyxEntry<Report>
return !existingIOUReport || hasIOUWaitingOnCurrentUserBankAccount(chatReport) || !canAddOrDeleteTransactions(existingIOUReport);
}

/**
* Checks if report contains actions with errors
*/
function hasActionsWithErrors(reportID: string): boolean {
const reportActions = ReportActionsUtils.getAllReportActions(reportID ?? '');
return Object.values(reportActions ?? {}).some((action) => !isEmptyObject(action.errors));
}

export {
getReportParticipantsTitle,
isReportMessageAttachment,
Expand Down Expand Up @@ -5627,6 +5635,7 @@ export {
canAddOrDeleteTransactions,
shouldCreateNewMoneyRequestReport,
isTrackExpenseReport,
hasActionsWithErrors,
};

export type {
Expand Down
25 changes: 0 additions & 25 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -754,31 +754,6 @@ function buildOnyxDataForMoneyRequest(
pendingFields: clearedPendingFields,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
value: {
...(isNewChatReport
? {
[chatCreatedAction.reportActionID]: {
// Disabling this line since transaction.filename can be an empty string
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
errors: getReceiptError(transaction?.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest),
},
[reportPreviewAction.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxError(null),
},
}
: {
[reportPreviewAction.reportActionID]: {
created: reportPreviewAction.created,
// Disabling this line since transaction.filename can be an empty string
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
errors: getReceiptError(transaction?.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest),
},
}),
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
Expand Down
Loading

0 comments on commit c5ac594

Please sign in to comment.