Skip to content

Commit

Permalink
Merge pull request #48171 from software-mansion-labs/rules/category-r…
Browse files Browse the repository at this point in the history
…ules

[OldDot Rules Migration] Category Rules
  • Loading branch information
marcaaron authored Sep 9, 2024
2 parents 201dc52 + b5ac473 commit 61ee515
Show file tree
Hide file tree
Showing 35 changed files with 1,875 additions and 107 deletions.
9 changes: 9 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2340,6 +2340,15 @@ const CONST = {
DEFAULT_MAX_EXPENSE_AGE: 90,
DEFAULT_MAX_EXPENSE_AMOUNT: 200000,
DEFAULT_MAX_AMOUNT_NO_RECEIPT: 2500,
REQUIRE_RECEIPTS_OVER_OPTIONS: {
DEFAULT: 'default',
NEVER: 'never',
ALWAYS: 'always',
},
EXPENSE_LIMIT_TYPES: {
EXPENSE: 'expense',
DAILY: 'daily',
},
},

CUSTOM_UNITS: {
Expand Down
6 changes: 6 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,10 @@ const ONYXKEYS = {
WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm',
WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm',
WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft',
WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM: 'workspaceCategoryDescriptionHintForm',
WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM_DRAFT: 'workspaceCategoryDescriptionHintFormDraft',
WORKSPACE_CATEGORY_FLAG_AMOUNTS_OVER_FORM: 'workspaceCategoryFlagAmountsOverForm',
WORKSPACE_CATEGORY_FLAG_AMOUNTS_OVER_FORM_DRAFT: 'workspaceCategoryFlagAmountsOverFormDraft',
WORKSPACE_TAG_FORM: 'workspaceTagForm',
WORKSPACE_TAG_FORM_DRAFT: 'workspaceTagFormDraft',
WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft',
Expand Down Expand Up @@ -677,6 +681,8 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName;
[ONYXKEYS.FORMS.WORKSPACE_COMPANY_CARD_FEED_NAME]: FormTypes.WorkspaceCompanyCardFeedName;
[ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM]: FormTypes.WorkspaceReportFieldForm;
[ONYXKEYS.FORMS.WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM]: FormTypes.WorkspaceCategoryDescriptionHintForm;
[ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FLAG_AMOUNTS_OVER_FORM]: FormTypes.WorkspaceCategoryFlagAmountsOverForm;
[ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm;
[ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm;
[ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm;
Expand Down
20 changes: 20 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,26 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/categories/:categoryName/gl-code',
getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/gl-code` as const,
},
WORSKPACE_CATEGORY_DEFAULT_TAX_RATE: {
route: 'settings/workspaces/:policyID/categories/:categoryName/tax-rate',
getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/tax-rate` as const,
},
WORSKPACE_CATEGORY_FLAG_AMOUNTS_OVER: {
route: 'settings/workspaces/:policyID/categories/:categoryName/flag-amounts',
getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/flag-amounts` as const,
},
WORSKPACE_CATEGORY_DESCRIPTION_HINT: {
route: 'settings/workspaces/:policyID/categories/:categoryName/description-hint',
getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/description-hint` as const,
},
WORSKPACE_CATEGORY_REQUIRE_RECEIPTS_OVER: {
route: 'settings/workspaces/:policyID/categories/:categoryName/require-receipts-over',
getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/require-receipts-over` as const,
},
WORSKPACE_CATEGORY_APPROVER: {
route: 'settings/workspaces/:policyID/categories/:categoryName/approver',
getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/approver` as const,
},
WORKSPACE_MORE_FEATURES: {
route: 'settings/workspaces/:policyID/more-features',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/more-features` as const,
Expand Down
5 changes: 5 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,11 @@ const SCREENS = {
CATEGORY_PAYROLL_CODE: 'Category_Payroll_Code',
CATEGORY_GL_CODE: 'Category_GL_Code',
CATEGORY_SETTINGS: 'Category_Settings',
CATEGORY_DEFAULT_TAX_RATE: 'Category_Default_Tax_Rate',
CATEGORY_FLAG_AMOUNTS_OVER: 'Category_Flag_Amounts_Over',
CATEGORY_DESCRIPTION_HINT: 'Category_Description_Hint',
CATEGORY_APPROVER: 'Category_Approver',
CATEGORY_REQUIRE_RECEIPTS_OVER: 'Category_Require_Receipts_Over',
CATEGORIES_SETTINGS: 'Categories_Settings',
CATEGORIES_IMPORT: 'Categories_Import',
CATEGORIES_IMPORTED: 'Categories_Imported',
Expand Down
115 changes: 115 additions & 0 deletions src/components/WorkspaceMembersSelectionList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, {useMemo} from 'react';
import type {SectionListData} from 'react-native';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import type {Icon} from '@src/types/onyx/OnyxCommon';
import Badge from './Badge';
import {FallbackAvatar} from './Icon/Expensicons';
import {usePersonalDetails} from './OnyxProvider';
import SelectionList from './SelectionList';
import InviteMemberListItem from './SelectionList/InviteMemberListItem';
import type {Section} from './SelectionList/types';

type SelectionListApprover = {
text: string;
alternateText: string;
keyForList: string;
isSelected: boolean;
login: string;
rightElement?: React.ReactNode;
icons: Icon[];
};
type ApproverSection = SectionListData<SelectionListApprover, Section<SelectionListApprover>>;

type WorkspaceMembersSelectionListProps = {
policyID: string;
selectedApprover: string;
setApprover: (email: string) => void;
};

function WorkspaceMembersSelectionList({policyID, selectedApprover, setApprover}: WorkspaceMembersSelectionListProps) {
const {translate} = useLocalize();
const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus();
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
const personalDetails = usePersonalDetails();
const policy = usePolicy(policyID);

const sections: ApproverSection[] = useMemo(() => {
const approvers: SelectionListApprover[] = [];

if (policy?.employeeList) {
const availableApprovers = Object.values(policy.employeeList)
.map((employee): SelectionListApprover | null => {
const isAdmin = employee?.role === CONST.REPORT.ROLE.ADMIN;
const email = employee.email;

if (!email) {
return null;
}

const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList);
const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? '');
const {avatar, displayName = email} = personalDetails?.[accountID] ?? {};

return {
text: displayName,
alternateText: email,
keyForList: email,
isSelected: selectedApprover === email,
login: email,
icons: [{source: avatar ?? FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}],
rightElement: isAdmin ? <Badge text={translate('common.admin')} /> : undefined,
};
})
.filter((approver): approver is SelectionListApprover => !!approver);

approvers.push(...availableApprovers);
}

const filteredApprovers =
debouncedSearchTerm !== ''
? approvers.filter((option) => {
const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(debouncedSearchTerm);
const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue);
return isPartOfSearchTerm;
})
: approvers;

return [
{
title: undefined,
data: OptionsListUtils.sortAlphabetically(filteredApprovers, 'text'),
shouldShow: true,
},
];
}, [debouncedSearchTerm, personalDetails, policy?.employeeList, selectedApprover, translate]);

const handleOnSelectRow = (approver: SelectionListApprover) => {
setApprover(approver.login);
};

const headerMessage = useMemo(() => (searchTerm && !sections[0].data.length ? translate('common.noResultsFound') : ''), [searchTerm, sections, translate]);

return (
<SelectionList
sections={sections}
ListItem={InviteMemberListItem}
textInputLabel={translate('selectionList.nameEmailOrPhoneNumber')}
textInputValue={searchTerm}
onChangeText={setSearchTerm}
headerMessage={headerMessage}
onSelectRow={handleOnSelectRow}
showScrollIndicator
showLoadingPlaceholder={!didScreenTransitionEnd}
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
/>
);
}

export default WorkspaceMembersSelectionList;
29 changes: 29 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3764,6 +3764,35 @@ export default {
unlockFeatureGoToSubtitle: 'Go to',
unlockFeatureEnableWorkflowsSubtitle: (featureName: string) => `and enable workflows, then add ${featureName} to unlock this feature.`,
},
categoryRules: {
title: 'Category rules',
approver: 'Approver',
requireDescription: 'Require description',
descriptionHint: 'Description hint',
descriptionHintDescription: (categoryName: string) =>
`Remind employees to provide additional information for “${categoryName}” spend. This hint appears in the description field on expenses.`,
descriptionHintLabel: 'Hint',
descriptionHintSubtitle: 'Pro-tip: The shorter the better!',
maxAmount: 'Max amount',
flagAmountsOver: 'Flag amounts over',
flagAmountsOverDescription: (categoryName) => `Applies to the category “${categoryName}”.`,
flagAmountsOverSubtitle: 'This overrides the max amount for all expenses.',
expenseLimitTypes: {
expense: 'Individual expense',
expenseSubtitle: 'Flag expense amounts by category. This rule overrides the general workspace rule for max expense amount.',
daily: 'Category total',
dailySubtitle: 'Flag total category spend per expense report.',
},
requireReceiptsOver: 'Require receipts over',
requireReceiptsOverList: {
default: (defaultAmount: string) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Default`,
never: 'Never require receipts',
always: 'Always require receipts',
},
defaultTaxRate: 'Default tax rate',
goTo: 'Go to',
andEnableWorkflows: 'and enable workflows, then add approvals to unlock this feature.',
},
},
},
getAssistancePage: {
Expand Down
29 changes: 29 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3814,6 +3814,35 @@ export default {
unlockFeatureGoToSubtitle: 'Ir a',
unlockFeatureEnableWorkflowsSubtitle: (featureName: string) => `y habilita flujos de trabajo, luego agrega ${featureName} para desbloquear esta función.`,
},
categoryRules: {
title: 'Reglas de categoría',
approver: 'Aprobador',
requireDescription: 'Requerir descripción',
descriptionHint: 'Sugerencia de descripción',
descriptionHintDescription: (categoryName: string) =>
`Recuerda a los empleados que deben proporcionar información adicional para los gastos de “${categoryName}”. Esta sugerencia aparece en el campo de descripción en los gastos.`,
descriptionHintLabel: 'Sugerencia',
descriptionHintSubtitle: 'Consejo: ¡Cuanto más corta, mejor!',
maxAmount: 'Importe máximo',
flagAmountsOver: 'Señala importes superiores a',
flagAmountsOverDescription: (categoryName: string) => `Aplica a la categoría “${categoryName}”.`,
flagAmountsOverSubtitle: 'Esto anula el importe máximo para todos los gastos.',
expenseLimitTypes: {
expense: 'Gasto individual',
expenseSubtitle: 'Señala importes de gastos por categoría. Esta regla anula la regla general del espacio de trabajo para el importe máximo de gastos.',
daily: 'Total por categoría',
dailySubtitle: 'Marcar el gasto total por categoría en cada informe de gastos.',
},
requireReceiptsOver: 'Requerir recibos para importes superiores a',
requireReceiptsOverList: {
default: (defaultAmount: string) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Predeterminado`,
never: 'Nunca requerir recibos',
always: 'Requerir recibos siempre',
},
defaultTaxRate: 'Tasa de impuesto predeterminada',
goTo: 'Ve a',
andEnableWorkflows: 'y habilita los flujos de trabajo, luego añade aprobaciones para desbloquear esta función.',
},
},
},
getAssistancePage: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type RemovePolicyCategoryReceiptsRequiredParams = {
policyID: string;
categoryName: string;
};

export default RemovePolicyCategoryReceiptsRequiredParams;
7 changes: 7 additions & 0 deletions src/libs/API/parameters/SetPolicyCategoryApproverParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type SetPolicyCategoryApproverParams = {
policyID: string;
categoryName: string;
approver: string;
};

export default SetPolicyCategoryApproverParams;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type SetPolicyCategoryDescriptionRequiredParams = {
policyID: string;
categoryName: string;
areCommentsRequired: boolean;
};

export default SetPolicyCategoryDescriptionRequiredParams;
10 changes: 10 additions & 0 deletions src/libs/API/parameters/SetPolicyCategoryMaxAmountParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type {PolicyCategoryExpenseLimitType} from '@src/types/onyx/PolicyCategory';

type SetPolicyCategoryMaxAmountParams = {
policyID: string;
categoryName: string;
maxExpenseAmount: number | null;
expenseLimitType: PolicyCategoryExpenseLimitType;
};

export default SetPolicyCategoryMaxAmountParams;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type SetPolicyCategoryReceiptsRequiredParams = {
policyID: string;
categoryName: string;
maxExpenseAmountNoReceipt: number;
};

export default SetPolicyCategoryReceiptsRequiredParams;
7 changes: 7 additions & 0 deletions src/libs/API/parameters/SetPolicyCategoryTaxParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type SetPolicyCategoryTaxParams = {
policyID: string;
categoryName: string;
taxID: string;
};

export default SetPolicyCategoryTaxParams;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type SetWorkspaceCategoryDescriptionHintParams = {
policyID: string;
categoryName: string;
commentHint: string;
};

export default SetWorkspaceCategoryDescriptionHintParams;
7 changes: 7 additions & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,17 @@ export type {default as ConfigureExpensifyCardsForPolicyParams} from './Configur
export type {default as CreateExpensifyCardParams} from './CreateExpensifyCardParams';
export type {default as UpdateExpensifyCardTitleParams} from './UpdateExpensifyCardTitleParams';
export type {default as OpenCardDetailsPageParams} from './OpenCardDetailsPageParams';
export type {default as SetPolicyCategoryDescriptionRequiredParams} from './SetPolicyCategoryDescriptionRequiredParams';
export type {default as SetPolicyCategoryApproverParams} from './SetPolicyCategoryApproverParams';
export type {default as SetWorkspaceCategoryDescriptionHintParams} from './SetWorkspaceCategoryDescriptionHintParams';
export type {default as SetPolicyCategoryTaxParams} from './SetPolicyCategoryTaxParams';
export type {default as SetPolicyCategoryMaxAmountParams} from './SetPolicyCategoryMaxAmountParams';
export type {default as EnablePolicyCompanyCardsParams} from './EnablePolicyCompanyCardsParams';
export type {default as ToggleCardContinuousReconciliationParams} from './ToggleCardContinuousReconciliationParams';
export type {default as CardDeactivateParams} from './CardDeactivateParams';
export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams';
export type {default as SetPolicyCategoryReceiptsRequiredParams} from './SetPolicyCategoryReceiptsRequiredParams';
export type {default as RemovePolicyCategoryReceiptsRequiredParams} from './RemovePolicyCategoryReceiptsRequiredParams';
export type {default as UpdateQuickbooksOnlineAutoCreateVendorParams} from './UpdateQuickbooksOnlineAutoCreateVendorParams';
export type {default as ImportCategoriesSpreadsheetParams} from './ImportCategoriesSpreadsheet';
export type {default as UpdateXeroGenericTypeParams} from './UpdateXeroGenericTypeParams';
Expand Down
14 changes: 14 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,13 @@ const WRITE_COMMANDS = {
SET_POLICY_BILLABLE_MODE: ' SetPolicyBillableMode',
DISABLE_POLICY_BILLABLE_MODE: 'DisablePolicyBillableExpenses',
SET_WORKSPACE_ERECEIPTS_ENABLED: 'SetWorkspaceEReceiptsEnabled',
SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED: 'SetPolicyCategoryDescriptionRequired',
SET_WORKSPACE_CATEGORY_DESCRIPTION_HINT: 'SetWorkspaceCategoryDescriptionHint',
SET_POLICY_CATEGORY_RECEIPTS_REQUIRED: 'SetPolicyCategoryReceiptsRequired',
REMOVE_POLICY_CATEGORY_RECEIPTS_REQUIRED: 'RemoveWorkspaceCategoryReceiptsRequired',
SET_POLICY_CATEGORY_MAX_AMOUNT: 'SetPolicyCategoryMaxAmount',
SET_POLICY_CATEGORY_APPROVER: 'SetPolicyCategoryApprover',
SET_POLICY_CATEGORY_TAX: 'SetPolicyCategoryTax',
SET_POLICY_TAXES_CURRENCY_DEFAULT: 'SetPolicyCurrencyDefaultTax',
SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT: 'SetPolicyForeignCurrencyDefaultTax',
SET_POLICY_CUSTOM_TAX_NAME: 'SetPolicyCustomTaxName',
Expand Down Expand Up @@ -583,6 +590,13 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS]: Parameters.EnablePolicyCompanyCardsParams;
[WRITE_COMMANDS.ENABLE_POLICY_INVOICING]: Parameters.EnablePolicyInvoicingParams;
[WRITE_COMMANDS.SET_POLICY_RULES_ENABLED]: Parameters.SetPolicyRulesEnabledParams;
[WRITE_COMMANDS.SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED]: Parameters.SetPolicyCategoryDescriptionRequiredParams;
[WRITE_COMMANDS.SET_WORKSPACE_CATEGORY_DESCRIPTION_HINT]: Parameters.SetWorkspaceCategoryDescriptionHintParams;
[WRITE_COMMANDS.SET_POLICY_CATEGORY_RECEIPTS_REQUIRED]: Parameters.SetPolicyCategoryReceiptsRequiredParams;
[WRITE_COMMANDS.REMOVE_POLICY_CATEGORY_RECEIPTS_REQUIRED]: Parameters.RemovePolicyCategoryReceiptsRequiredParams;
[WRITE_COMMANDS.SET_POLICY_CATEGORY_MAX_AMOUNT]: Parameters.SetPolicyCategoryMaxAmountParams;
[WRITE_COMMANDS.SET_POLICY_CATEGORY_APPROVER]: Parameters.SetPolicyCategoryApproverParams;
[WRITE_COMMANDS.SET_POLICY_CATEGORY_TAX]: Parameters.SetPolicyCategoryTaxParams;
[WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams;
[WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams;
[WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams;
Expand Down
Loading

0 comments on commit 61ee515

Please sign in to comment.