Skip to content

Commit

Permalink
Merge pull request #47468 from software-mansion-labs/brtqkr/expense-r…
Browse files Browse the repository at this point in the history
…eports-section

 [OldDot Rules Migration] Expense report rules
  • Loading branch information
marcaaron authored Sep 6, 2024
2 parents 67333b9 + 442d734 commit c1da839
Show file tree
Hide file tree
Showing 43 changed files with 1,859 additions and 87 deletions.
9 changes: 8 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ const CONST = {
SAGE_INTACCT_HELP_LINK:
"https://help.expensify.com/articles/expensify-classic/connections/sage-intacct/Sage-Intacct-Troubleshooting#:~:text=First%20make%20sure%20that%20you,your%20company's%20Web%20Services%20authorizations.",
PRICING: `https://www.expensify.com/pricing`,

CUSTOM_REPORT_NAME_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates',
// Use Environment.getEnvironmentURL to get the complete URL with port number
DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:',
OLDDOT_URLS: {
Expand Down Expand Up @@ -2055,11 +2055,18 @@ const CONST = {
// Often referred to as "collect" workspaces
TEAM: 'team',
},
FIELD_LIST_TITLE_FIELD_ID: 'text_title',
DEFAULT_REPORT_NAME_PATTERN: '{report:type} {report:startdate}',
ROLE: {
ADMIN: 'admin',
AUDITOR: 'auditor',
USER: 'user',
},
AUTO_REIMBURSEMENT_MAX_LIMIT_CENTS: 2000000,
AUTO_REIMBURSEMENT_DEFAULT_LIMIT_CENTS: 10000,
AUTO_APPROVE_REPORTS_UNDER_DEFAULT_CENTS: 10000,
RANDOM_AUDIT_DEFAULT_PERCENTAGE: 5,

AUTO_REPORTING_FREQUENCIES: {
INSTANT: 'instant',
IMMEDIATE: 'immediate',
Expand Down
12 changes: 12 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,14 @@ const ONYXKEYS = {
SEARCH_ADVANCED_FILTERS_FORM_DRAFT: 'searchAdvancedFiltersFormDraft',
TEXT_PICKER_MODAL_FORM: 'textPickerModalForm',
TEXT_PICKER_MODAL_FORM_DRAFT: 'textPickerModalFormDraft',
RULES_CUSTOM_NAME_MODAL_FORM: 'rulesCustomNameModalForm',
RULES_CUSTOM_NAME_MODAL_FORM_DRAFT: 'rulesCustomNameModalFormDraft',
RULES_AUTO_APPROVE_REPORTS_UNDER_MODAL_FORM: 'rulesAutoApproveReportsUnderModalForm',
RULES_AUTO_APPROVE_REPORTS_UNDER_MODAL_FORM_DRAFT: 'rulesAutoApproveReportsUnderModalFormDraft',
RULES_RANDOM_REPORT_AUDIT_MODAL_FORM: 'rulesRandomReportAuditModalForm',
RULES_RANDOM_REPORT_AUDIT_MODAL_FORM_DRAFT: 'rulesRandomReportAuditModalFormDraft',
RULES_AUTO_PAY_REPORTS_UNDER_MODAL_FORM: 'rulesAutoPayReportsUnderModalForm',
RULES_AUTO_PAY_REPORTS_UNDER_MODAL_FORM_DRAFT: 'rulesAutoPayReportsUnderModalFormDraft',
RULES_REQUIRED_RECEIPT_AMOUNT_FORM: 'rulesRequiredReceiptAmountForm',
RULES_REQUIRED_RECEIPT_AMOUNT_FORM_DRAFT: 'rulesRequiredReceiptAmountFormDraft',
RULES_MAX_EXPENSE_AMOUNT_FORM: 'rulesMaxExpenseAmountForm',
Expand Down Expand Up @@ -732,6 +740,10 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.SAGE_INTACCT_DIMENSION_TYPE_FORM]: FormTypes.SageIntacctDimensionForm;
[ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM]: FormTypes.SearchAdvancedFiltersForm;
[ONYXKEYS.FORMS.TEXT_PICKER_MODAL_FORM]: FormTypes.TextPickerModalForm;
[ONYXKEYS.FORMS.RULES_CUSTOM_NAME_MODAL_FORM]: FormTypes.RulesCustomNameModalForm;
[ONYXKEYS.FORMS.RULES_AUTO_APPROVE_REPORTS_UNDER_MODAL_FORM]: FormTypes.RulesAutoApproveReportsUnderModalForm;
[ONYXKEYS.FORMS.RULES_RANDOM_REPORT_AUDIT_MODAL_FORM]: FormTypes.RulesRandomReportAuditModalForm;
[ONYXKEYS.FORMS.RULES_AUTO_PAY_REPORTS_UNDER_MODAL_FORM]: FormTypes.RulesAutoPayReportsUnderModalForm;
[ONYXKEYS.FORMS.RULES_REQUIRED_RECEIPT_AMOUNT_FORM]: FormTypes.RulesRequiredReceiptAmountForm;
[ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AMOUNT_FORM]: FormTypes.RulesMaxExpenseAmountForm;
[ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AGE_FORM]: FormTypes.RulesMaxExpenseAgeForm;
Expand Down
16 changes: 16 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,22 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/distance-rates/:rateID/tax-rate/edit',
getRoute: (policyID: string, rateID: string) => `settings/workspaces/${policyID}/distance-rates/${rateID}/tax-rate/edit` as const,
},
RULES_CUSTOM_NAME: {
route: 'settings/workspaces/:policyID/rules/name',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/name` as const,
},
RULES_AUTO_APPROVE_REPORTS_UNDER: {
route: 'settings/workspaces/:policyID/rules/auto-approve',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/auto-approve` as const,
},
RULES_RANDOM_REPORT_AUDIT: {
route: 'settings/workspaces/:policyID/rules/audit',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/audit` as const,
},
RULES_AUTO_PAY_REPORTS_UNDER: {
route: 'settings/workspaces/:policyID/rules/auto-pay',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/auto-pay` as const,
},
RULES_RECEIPT_REQUIRED_AMOUNT: {
route: 'settings/workspaces/:policyID/rules/receipt-required-amount',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/receipt-required-amount` as const,
Expand Down
4 changes: 4 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,10 @@ const SCREENS = {
DISTANCE_RATE_TAX_RATE_EDIT: 'Distance_Rate_Tax_Rate_Edit',
UPGRADE: 'Workspace_Upgrade',
RULES: 'Policy_Rules',
RULES_CUSTOM_NAME: 'Rules_Custom_Name',
RULES_AUTO_APPROVE_REPORTS_UNDER: 'Rules_Auto_Approve_Reports_Under',
RULES_RANDOM_REPORT_AUDIT: 'Rules_Random_Report_Audit',
RULES_AUTO_PAY_REPORTS_UNDER: 'Rules_AutoPay_Reports_Under',
RULES_RECEIPT_REQUIRED_AMOUNT: 'Rules_Receipt_Required_Amount',
RULES_MAX_EXPENSE_AMOUNT: 'Rules_Max_Expense_Amount',
RULES_MAX_EXPENSE_AGE: 'Rules_Max_Expense_Age',
Expand Down
9 changes: 5 additions & 4 deletions src/components/AmountForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ type AmountFormProps = {
/** Custom max amount length. It defaults to CONST.IOU.AMOUNT_MAX_LENGTH */
amountMaxLength?: number;

/** Custom label for the TextInput */
label?: string;

/** Whether the form should use a standard TextInput as a base */
displayAsTextInput?: boolean;
} & Pick<TextInputWithCurrencySymbolProps, 'hideCurrencySymbol' | 'extraSymbol'> &
Pick<BaseTextInputProps, 'autoFocus'>;
Pick<BaseTextInputProps, 'autoFocus' | 'hasError'>;

/**
* Returns the new selection object based on the updated amount's length
Expand All @@ -67,7 +69,6 @@ function AmountForm(
currency = CONST.CURRENCY.USD,
extraDecimals = 0,
amountMaxLength,
errorText,
onInputChange,
onCurrencyButtonPress,
displayAsTextInput = false,
Expand Down Expand Up @@ -296,11 +297,11 @@ function AmountForm(
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
/>
{!!errorText && (
{!!rest.errorText && (
<FormHelpMessage
style={[styles.pAbsolute, styles.b0, canUseTouchScreen ? styles.mb0 : styles.mb3, styles.ph5, styles.w100]}
isError
message={errorText}
message={rest.errorText}
/>
)}
</View>
Expand Down
52 changes: 52 additions & 0 deletions src/components/BulletList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type {ReactNode} from 'react';
import React from 'react';
import {View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import Text from './Text';

type BulletListItem = string;

type BulletListProps = {
/** List of items for the list. Each item will be rendered as a sepearte point. */
items: BulletListItem[];

/** Header section of the list */
header: string | ReactNode;
};

function BulletList({items, header}: BulletListProps) {
const styles = useThemeStyles();

const baseTextStyles = [styles.mutedNormalTextLabel];

const renderBulletListHeader = () => {
if (typeof header === 'string') {
return <Text style={baseTextStyles}>{header}</Text>;
}
return header;
};

const renderBulletPoint = (item: string) => {
return (
<Text
style={baseTextStyles}
key={item}
>
<Text style={[styles.ph2, baseTextStyles]}></Text>
{item}
</Text>
);
};

return (
<View style={[styles.w100, styles.mt2]}>
{renderBulletListHeader()}
<View>{items.map((item) => renderBulletPoint(item))}</View>
</View>
);
}

BulletList.displayName = 'BulletList';

export type {BulletListProps};
export default BulletList;
2 changes: 2 additions & 0 deletions src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type CountrySelector from '@components/CountrySelector';
import type CurrencySelector from '@components/CurrencySelector';
import type DatePicker from '@components/DatePicker';
import type EmojiPickerButtonDropdown from '@components/EmojiPicker/EmojiPickerButtonDropdown';
import type PercentageForm from '@components/PercentageForm';
import type Picker from '@components/Picker';
import type RadioButtons from '@components/RadioButtons';
import type RoomNameInput from '@components/RoomNameInput';
Expand Down Expand Up @@ -42,6 +43,7 @@ type ValidInputs =
| typeof CountrySelector
| typeof CurrencySelector
| typeof AmountForm
| typeof PercentageForm
| typeof BusinessTypePicker
| typeof DimensionTypeSelector
| typeof StateSelector
Expand Down
102 changes: 102 additions & 0 deletions src/components/PercentageForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useMemo, useRef, useState} from 'react';
import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
import CONST from '@src/CONST';
import TextInput from './TextInput';
import type {BaseTextInputRef} from './TextInput/BaseTextInput/types';

type PercentageFormProps = {
/** Amount supplied by the FormProvider */
value?: string;

/** Error to display at the bottom of the component */
errorText?: string;

/** Callback to update the amount in the FormProvider */
onInputChange?: (value: string) => void;

/** Custom label for the TextInput */
label?: string;
};

/**
* Returns the new selection object based on the updated amount's length
*/
const getNewSelection = (oldSelection: {start: number; end: number}, prevLength: number, newLength: number) => {
const cursorPosition = oldSelection.end + (newLength - prevLength);
return {start: cursorPosition, end: cursorPosition};
};

function PercentageForm({value: amount, errorText, onInputChange, label, ...rest}: PercentageFormProps, forwardedRef: ForwardedRef<BaseTextInputRef>) {
const {toLocaleDigit, numberFormat} = useLocalize();

const textInput = useRef<BaseTextInputRef | null>(null);

const currentAmount = useMemo(() => (typeof amount === 'string' ? amount : ''), [amount]);

const [selection, setSelection] = useState({
start: currentAmount.length,
end: currentAmount.length,
});

const forwardDeletePressedRef = useRef(false);

/**
* Sets the selection and the amount accordingly to the value passed to the input
* @param newAmount - Changed amount from user input
*/
const setNewAmount = useCallback(
(newAmount: string) => {
// Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value
// More info: https://github.com/Expensify/App/issues/16974
const newAmountWithoutSpaces = MoneyRequestUtils.stripSpacesFromAmount(newAmount);
// Use a shallow copy of selection to trigger setSelection
// More info: https://github.com/Expensify/App/issues/16385
if (!MoneyRequestUtils.validatePercentage(newAmountWithoutSpaces)) {
setSelection((prevSelection) => ({...prevSelection}));
return;
}

const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces);
const isForwardDelete = currentAmount.length > strippedAmount.length && forwardDeletePressedRef.current;
setSelection(getNewSelection(selection, isForwardDelete ? strippedAmount.length : currentAmount.length, strippedAmount.length));
onInputChange?.(strippedAmount);
},
[currentAmount, onInputChange, selection],
);

const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit);

return (
<TextInput
label={label}
value={formattedAmount}
onChangeText={setNewAmount}
placeholder={numberFormat(0)}
ref={(ref: BaseTextInputRef) => {
if (typeof forwardedRef === 'function') {
forwardedRef(ref);
} else if (forwardedRef && 'current' in forwardedRef) {
// eslint-disable-next-line no-param-reassign
forwardedRef.current = ref;
}
textInput.current = ref;
}}
selection={selection}
onSelectionChange={(e: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
setSelection(e.nativeEvent.selection);
}}
suffixCharacter="%"
keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
/>
);
}

PercentageForm.displayName = 'PercentageForm';

export default forwardRef(PercentageForm);
export type {PercentageFormProps};
Loading

0 comments on commit c1da839

Please sign in to comment.