Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5771,6 +5771,7 @@ const CONST = {
RECEIPT_GENERATED_WITH_AI: 'receiptGeneratedWithAI',
OVER_TRIP_LIMIT: 'overTripLimit',
COMPANY_CARD_REQUIRED: 'companyCardRequired',
MISSING_ATTENDEES: 'missingAttendees',
},
RTER_VIOLATION_TYPES: {
BROKEN_CARD_CONNECTION: 'brokenCardConnection',
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1967,6 +1967,10 @@ const ROUTES = {
route: 'workspaces/:policyID/category/:categoryName/require-receipts-over',
getRoute: (policyID: string, categoryName: string) => `workspaces/${policyID}/category/${encodeURIComponent(categoryName)}/require-receipts-over` as const,
},
WORKSPACE_CATEGORY_REQUIRED_FIELDS: {
route: 'workspaces/:policyID/category/:categoryName/required-fields',
getRoute: (policyID: string, categoryName: string) => `workspaces/${policyID}/category/${encodeURIComponent(categoryName)}/required-fields` as const,
},
WORKSPACE_CATEGORY_APPROVER: {
route: 'workspaces/:policyID/category/:categoryName/approver',
getRoute: (policyID: string, categoryName: string) => `workspaces/${policyID}/category/${encodeURIComponent(categoryName)}/approver` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@ const SCREENS = {
CATEGORY_DESCRIPTION_HINT: 'Category_Description_Hint',
CATEGORY_APPROVER: 'Category_Approver',
CATEGORY_REQUIRE_RECEIPTS_OVER: 'Category_Require_Receipts_Over',
CATEGORY_REQUIRED_FIELDS: 'Category_Required_Fields',
CATEGORIES_SETTINGS: 'Categories_Settings',
CATEGORIES_IMPORT: 'Categories_Import',
CATEGORIES_IMPORTED: 'Categories_Imported',
Expand Down
13 changes: 13 additions & 0 deletions src/components/MoneyRequestConfirmationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
setMoneyRequestTaxRate,
setSplitShares,
} from '@libs/actions/IOU';
import {getIsMissingAttendeesViolation} from '@libs/AttendeeUtils';
import {isCategoryDescriptionRequired} from '@libs/CategoryUtils';
import {convertToBackendAmount, convertToDisplayString, convertToDisplayStringWithoutCurrency, getCurrencyDecimals} from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
Expand Down Expand Up @@ -920,6 +921,12 @@ function MoneyRequestConfirmationList({
return;
}

const isMissingAttendeesViolation = getIsMissingAttendeesViolation(policyCategories, iouCategory, iouAttendees, currentUserPersonalDetails, policy?.isAttendeeTrackingEnabled);
if (isMissingAttendeesViolation) {
setFormError('violations.missingAttendees');
return;
}

if (isPerDiemRequest && (transaction.comment?.customUnit?.subRates ?? []).length === 0) {
setFormError('iou.error.invalidSubrateLength');
return;
Expand Down Expand Up @@ -993,6 +1000,8 @@ function MoneyRequestConfirmationList({
showDelegateNoAccessModal,
iouCategory,
policyCategories,
iouAttendees,
currentUserPersonalDetails,
],
);

Expand All @@ -1016,6 +1025,10 @@ function MoneyRequestConfirmationList({
if (isTypeSplit && !shouldShowReadOnlySplits) {
return debouncedFormError && translate(debouncedFormError);
}
// Don't show error at the bottom of the form for missing attendees
if (formError === 'violations.missingAttendees') {
return;
}
return formError && translate(formError);
}, [routeError, isTypeSplit, shouldShowReadOnlySplits, debouncedFormError, formError, translate]);

Expand Down
7 changes: 5 additions & 2 deletions src/components/MoneyRequestConfirmationListFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ function MoneyRequestConfirmationListFooter({
const shouldDisplayDistanceRateError = formError === 'iou.error.invalidRate';
const shouldDisplayTagError = formError === 'violations.tagOutOfPolicy';
const shouldDisplayCategoryError = formError === 'violations.categoryOutOfPolicy';
const shouldDisplayAttendeesError = formError === 'violations.missingAttendees';

const showReceiptEmptyState = shouldShowReceiptEmptyState(iouType, action, policy, isPerDiemRequest);
// The per diem custom unit
Expand Down Expand Up @@ -727,7 +728,7 @@ function MoneyRequestConfirmationListFooter({
item: (
<MenuItemWithTopDescription
key="attendees"
shouldShowRightIcon
shouldShowRightIcon={!isReadOnly}
title={iouAttendees?.map((item) => item?.displayName ?? item?.login).join(', ')}
description={`${translate('iou.attendees')} ${
iouAttendees?.length && iouAttendees.length > 1 && formattedAmountPerAttendee ? `\u00B7 ${formattedAmountPerAttendee} ${translate('common.perPerson')}` : ''
Expand All @@ -741,8 +742,10 @@ function MoneyRequestConfirmationListFooter({

Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()));
}}
interactive
interactive={!isReadOnly}
shouldRenderAsHTML
brickRoadIndicator={shouldDisplayAttendeesError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={shouldDisplayAttendeesError ? translate(formError) : ''}
/>
),
shouldShow: shouldShowAttendees,
Expand Down
25 changes: 20 additions & 5 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionViolations from '@hooks/useTransactionViolations';
import type {ViolationField} from '@hooks/useViolations';
import useViolations from '@hooks/useViolations';
import {initSplitExpense, updateMoneyRequestBillable, updateMoneyRequestReimbursable} from '@libs/actions/IOU/index';
import {getIsMissingAttendeesViolation} from '@libs/AttendeeUtils';
import {filterPersonalCards, getCompanyCardDescription} from '@libs/CardUtils';
import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils';
import {convertToDisplayString} from '@libs/CurrencyUtils';
Expand All @@ -51,13 +53,12 @@ import {
isTaxTrackingEnabled,
} from '@libs/PolicyUtils';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {computeReportName} from '@libs/ReportNameUtils';
import {isSplitAction} from '@libs/ReportSecondaryActionUtils';
import {
canEditFieldOfMoneyRequest,
canEditMoneyRequest,
canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
// eslint-disable-next-line @typescript-eslint/no-deprecated
getReportName,
getTransactionDetails,
getTripIDFromTransactionParentReportID,
isExpenseReport,
Expand Down Expand Up @@ -97,7 +98,6 @@ import {
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
import Navigation from '@navigation/Navigation';
import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground';
import {initSplitExpense, updateMoneyRequestBillable, updateMoneyRequestReimbursable} from '@userActions/IOU';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -167,6 +167,7 @@ function MoneyRequestView({
const {getReportRHPActiveRoute} = useActiveRoute();
const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH, {canBeMissing: true});

const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false});

const searchContext = useSearchContext();
Expand Down Expand Up @@ -472,6 +473,13 @@ function MoneyRequestView({
const hasErrors = hasMissingSmartscanFields(transaction);
// Need to return undefined when we have pendingAction to avoid the duplicate pending action
const getPendingFieldAction = (fieldPath: TransactionPendingFieldsKey) => (pendingAction ? undefined : transaction?.pendingFields?.[fieldPath]);
const isMissingAttendeesViolation = getIsMissingAttendeesViolation(
policyCategories,
updatedTransaction?.category ?? categoryForDisplay,
actualAttendees,
currentUserPersonalDetails,
policy?.isAttendeeTrackingEnabled,
);

const getErrorForField = (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string) => {
// Checks applied when creating a new expense
Expand Down Expand Up @@ -512,6 +520,10 @@ function MoneyRequestView({
return `${violations.map((violation) => ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL)).join('. ')}.`;
}

if (field === 'attendees' && isMissingAttendeesViolation) {
return translate('violations.missingAttendees');
}

return '';
};

Expand Down Expand Up @@ -720,8 +732,9 @@ function MoneyRequestView({
);
});

// eslint-disable-next-line @typescript-eslint/no-deprecated
const reportNameToDisplay = isFromMergeTransaction ? (updatedTransaction?.reportName ?? translate('common.none')) : (parentReport?.reportName ?? getReportName(parentReport));
const reportNameToDisplay = isFromMergeTransaction
? (updatedTransaction?.reportName ?? translate('common.none'))
: (parentReport?.reportName ?? computeReportName(parentReport, allReports, allPolicies, allTransactions));
const shouldShowReport = !!parentReportID || (isFromMergeTransaction && !!reportNameToDisplay);
const reportCopyValue = !canEditReport && reportNameToDisplay !== translate('common.none') ? reportNameToDisplay : undefined;
const shouldShowCategoryAnalyzing = isCategoryBeingAnalyzed(updatedTransaction ?? transaction);
Expand Down Expand Up @@ -1012,6 +1025,8 @@ function MoneyRequestView({
onPress={() => {
Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport?.reportID));
}}
brickRoadIndicator={getErrorForField('attendees') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={getErrorForField('attendees')}
interactive={canEdit}
shouldShowRightIcon={canEdit}
shouldRenderAsHTML
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import useThemeStyles from '@hooks/useThemeStyles';
import type {TransactionPreviewData} from '@libs/actions/Search';
import {handleActionButtonPress as handleActionButtonPressUtil} from '@libs/actions/Search';
import {syncMissingAttendeesViolation} from '@libs/AttendeeUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {isViolationDismissed, shouldShowViolation} from '@libs/TransactionUtils';
import variables from '@styles/variables';
Expand Down Expand Up @@ -65,6 +66,8 @@
const snapshotPolicy = useMemo(() => {
return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy;
}, [snapshot, transactionItem.policyID]);
// Fetch policy categories directly from Onyx since they are not included in the search snapshot
const [policyCategories] = originalUseOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${transactionItem.policyID}`, {canBeMissing: true});
const [lastPaymentMethod] = useOnyx(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {canBeMissing: true});
const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true});

Expand Down Expand Up @@ -121,12 +124,25 @@
]);

const transactionViolations = useMemo(() => {
return (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter(
const onyxViolations = (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter(
(violation: TransactionViolation) =>
!isViolationDismissed(transactionItem, violation, currentUserDetails.email ?? '', currentUserDetails.accountID, snapshotReport, snapshotPolicy) &&
shouldShowViolation(snapshotReport, snapshotPolicy, violation.name, currentUserDetails.email ?? '', false, transactionItem),
);
}, [snapshotPolicy, snapshotReport, transactionItem, violations, currentUserDetails.email, currentUserDetails.accountID]);

// Sync missingAttendees violation with current policy category settings (can be removed later when BE handles this)
const attendeeOnyxViolations = syncMissingAttendeesViolation(
onyxViolations,
policyCategories,
transactionItem.category ?? '',
transactionItem.attendees,
currentUserDetails,
snapshotPolicy?.isAttendeeTrackingEnabled ?? false,
snapshotPolicy?.type === CONST.POLICY.TYPE.CORPORATE,
);

return [...onyxViolations, ...attendeeOnyxViolations];
}, [snapshotPolicy, policyCategories, snapshotReport, transactionItem, violations, currentUserDetails.email, currentUserDetails.accountID, currentUserDetails]);

Check warning on line 145 in src/components/SelectionListWithSections/Search/TransactionListItem.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

React Hook useMemo has unnecessary dependencies: 'currentUserDetails.accountID' and 'currentUserDetails.email'. Either exclude them or remove the dependency array

Check warning on line 145 in src/components/SelectionListWithSections/Search/TransactionListItem.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

React Hook useMemo has unnecessary dependencies: 'currentUserDetails.accountID' and 'currentUserDetails.email'. Either exclude them or remove the dependency array

const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);

Expand Down
3 changes: 2 additions & 1 deletion src/hooks/useViolations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {TransactionViolation, ViolationName} from '@src/types/onyx';
/**
* Names of Fields where violations can occur.
*/
const validationFields = ['amount', 'billable', 'category', 'comment', 'date', 'merchant', 'receipt', 'tag', 'tax', 'customUnitRateID', 'none'] as const;
const validationFields = ['amount', 'billable', 'category', 'comment', 'date', 'merchant', 'receipt', 'tag', 'tax', 'attendees', 'customUnitRateID', 'none'] as const;

type ViolationField = TupleToUnion<typeof validationFields>;

Expand All @@ -28,6 +28,7 @@ const violationNameToField: Record<ViolationName, (violation: TransactionViolati
maxAge: () => 'date',
missingCategory: () => 'category',
missingComment: () => 'comment',
missingAttendees: () => 'attendees',
missingTag: () => 'tag',
modifiedAmount: () => 'amount',
modifiedDate: () => 'date',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6219,6 +6219,10 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
title: 'Kategorierichtlinien',
approver: 'Genehmiger',
requireDescription: 'Beschreibung erforderlich',
requireFields: 'Felder verpflichtend machen',
requiredFieldsTitle: 'Pflichtfelder',
requiredFieldsDescription: (categoryName: string) => `Dies gilt für alle Ausgaben, die als <strong>${categoryName}</strong> kategorisiert sind.`,
requireAttendees: 'Teilnehmer erforderlich machen',
descriptionHint: 'Hinweis zur Beschreibung',
descriptionHintDescription: (categoryName: string) =>
`Mitarbeitende daran erinnern, zusätzliche Informationen für Ausgaben der Kategorie „${categoryName}“ anzugeben. Dieser Hinweis erscheint im Beschreibungsfeld von Ausgaben.`,
Expand Down Expand Up @@ -7271,6 +7275,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Datum älter als ${maxAge} Tage`,
missingCategory: 'Fehlende Kategorie',
missingComment: 'Beschreibung für ausgewählte Kategorie erforderlich',
missingAttendees: 'Für diese Kategorie sind mehrere Teilnehmer erforderlich',
missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Fehlende ${tagName ?? 'Tag'}`,
modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => {
switch (type) {
Expand Down
5 changes: 5 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6079,6 +6079,10 @@ const translations = {
title: 'Category rules',
approver: 'Approver',
requireDescription: 'Require description',
requireFields: 'Require fields',
requiredFieldsTitle: 'Required fields',
requiredFieldsDescription: (categoryName: string) => `This will apply to all expenses categorized as <strong>${categoryName}</strong>.`,
requireAttendees: 'Require attendees',
descriptionHint: 'Description hint',
descriptionHintDescription: (categoryName: string) =>
`Remind employees to provide additional information for “${categoryName}” spend. This hint appears in the description field on expenses.`,
Expand Down Expand Up @@ -7163,6 +7167,7 @@ const translations = {
maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Date older than ${maxAge} days`,
missingCategory: 'Missing category',
missingComment: 'Description required for selected category',
missingAttendees: 'Multiple attendees required for this category',
missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Missing ${tagName ?? 'tag'}`,
modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => {
switch (type) {
Expand Down
5 changes: 5 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5816,6 +5816,10 @@ ${amount} para ${merchant} - ${date}`,
title: 'Reglas de categoría',
approver: 'Aprobador',
requireDescription: 'Requerir descripción',
requireFields: 'Requerir campos',
requiredFieldsTitle: 'Campos obligatorios',
requiredFieldsDescription: (categoryName) => `Esto se aplicará a todos los gastos categorizados como <strong>${categoryName}</strong>.`,
requireAttendees: 'Requerir asistentes',
descriptionHint: 'Sugerencia de descripción',
descriptionHintDescription: (categoryName) =>
`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.`,
Expand Down Expand Up @@ -7303,6 +7307,7 @@ ${amount} para ${merchant} - ${date}`,
maxAge: ({maxAge}) => `Fecha de más de ${maxAge} días`,
missingCategory: 'Falta categoría',
missingComment: 'Descripción obligatoria para la categoría seleccionada',
missingAttendees: 'Se requieren múltiples asistentes para esta categoría',
missingTag: ({tagName} = {}) => `Falta ${tagName ?? 'etiqueta'}`,
modifiedAmount: ({type, displayPercentVariance}) => {
switch (type) {
Expand Down
5 changes: 5 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6227,6 +6227,10 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
title: 'Règles de catégorie',
approver: 'Approbateur',
requireDescription: 'Description requise',
requireFields: 'Rendre les champs obligatoires',
requiredFieldsTitle: 'Champs obligatoires',
requiredFieldsDescription: (categoryName: string) => `Cela s’appliquera à toutes les dépenses classées dans la catégorie <strong>${categoryName}</strong>.`,
requireAttendees: 'Exiger des participants',
descriptionHint: 'Indice de description',
descriptionHintDescription: (categoryName: string) =>
`Rappelez aux employés de fournir des informations supplémentaires pour les dépenses « ${categoryName} ». Cet indice apparaît dans le champ de description des dépenses.`,
Expand Down Expand Up @@ -7281,6 +7285,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Date de plus de ${maxAge} jours`,
missingCategory: 'Catégorie manquante',
missingComment: 'Description requise pour la catégorie sélectionnée',
missingAttendees: 'Plusieurs participants sont requis pour cette catégorie',
missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Manquant ${tagName ?? 'étiquette'}`,
modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => {
switch (type) {
Expand Down
Loading
Loading