diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 836b321d7018e..43b400c4a4da2 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -910,6 +910,8 @@ const ONYXKEYS = { WORKSPACE_INVOICES_COMPANY_NAME_FORM_DRAFT: 'workspaceInvoicesCompanyNameFormDraft', WORKSPACE_INVOICES_COMPANY_WEBSITE_FORM: 'workspaceInvoicesCompanyWebsiteForm', WORKSPACE_INVOICES_COMPANY_WEBSITE_FORM_DRAFT: 'workspaceInvoicesCompanyWebsiteFormDraft', + WORKSPACE_TIME_TRACKING_DEFAULT_RATE_FORM: 'workspaceTimeTrackingDefaultRateForm', + WORKSPACE_TIME_TRACKING_DEFAULT_RATE_FORM_DRAFT: 'workspaceTimeTrackingDefaultRateFormDraft', NEW_CHAT_NAME_FORM: 'newChatNameForm', NEW_CHAT_NAME_FORM_DRAFT: 'newChatNameFormDraft', SUBSCRIPTION_SIZE_FORM: 'subscriptionSizeForm', @@ -1070,6 +1072,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM]: FormTypes.WorkspaceTaxValueForm; [ONYXKEYS.FORMS.WORKSPACE_INVOICES_COMPANY_NAME_FORM]: FormTypes.WorkspaceInvoicesCompanyNameForm; [ONYXKEYS.FORMS.WORKSPACE_INVOICES_COMPANY_WEBSITE_FORM]: FormTypes.WorkspaceInvoicesCompanyWebsiteForm; + [ONYXKEYS.FORMS.WORKSPACE_TIME_TRACKING_DEFAULT_RATE_FORM]: FormTypes.WorkspaceTimeTrackingDefaultRateForm; [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; [ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 55a066f38705f..2852ffcfe8fbf 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2624,6 +2624,21 @@ const ROUTES = { route: 'workspaces/:policyID/per-diem/edit/currency/:rateID/:subRateID', getRoute: (policyID: string, rateID: string, subRateID: string) => `workspaces/${policyID}/per-diem/edit/currency/${rateID}/${subRateID}` as const, }, + + WORKSPACE_TIME_TRACKING: { + route: 'workspaces/:policyID/time-tracking', + getRoute: (policyID: string | undefined) => { + if (!policyID) { + Log.warn('Invalid policyID is used to build the WORKSPACE_TIME_TRACKING route'); + } + return `workspaces/${policyID}/time-tracking` as const; + }, + }, + WORKSPACE_TIME_TRACKING_DEFAULT_RATE: { + route: 'workspaces/:policyID/time-tracking/rate', + getRoute: (policyID: string) => `workspaces/${policyID}/time-tracking/rate` as const, + }, + REPORTS_DEFAULT_TITLE: { route: 'workspaces/:policyID/reports/name', getRoute: (policyID: string) => `workspaces/${policyID}/reports/name` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 1d0fd82f53449..d012bf8f799c7 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -779,6 +779,8 @@ const SCREENS = { PER_DIEM_EDIT_SUBRATE: 'Per_Diem_Edit_Subrate', PER_DIEM_EDIT_AMOUNT: 'Per_Diem_Edit_Amount', PER_DIEM_EDIT_CURRENCY: 'Per_Diem_Edit_Currency', + TIME_TRACKING: 'Time_Tracking', + TIME_TRACKING_DEFAULT_RATE: 'Time_Tracking_Default_Rate', }, EDIT_REQUEST: { diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts index cb901ace045d8..ece66025bfd25 100644 --- a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts +++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts @@ -37,6 +37,7 @@ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [ SCREENS.WORKSPACE.RULES, SCREENS.WORKSPACE.PER_DIEM, SCREENS.WORKSPACE.RECEIPT_PARTNERS, + SCREENS.WORKSPACE.TIME_TRACKING, ]; export default WIDE_LAYOUT_INACTIVE_SCREENS; diff --git a/src/languages/de.ts b/src/languages/de.ts index 332997202796a..344ef368b9284 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5304,7 +5304,11 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU title: 'Regeln', subtitle: 'Belege verlangen, hohe Ausgaben kennzeichnen und mehr.', }, - timeTracking: {title: 'Zeit', subtitle: 'Legen Sie einen abrechenbaren Stundensatz fest, damit Mitarbeitende für ihre Zeit bezahlt werden.'}, + timeTracking: { + title: 'Zeit', + subtitle: 'Legen Sie einen abrechenbaren Stundensatz für die Zeiterfassung fest.', + defaultHourlyRate: 'Standardstundensatz', + }, }, reports: { reportsCustomTitleExamples: 'Beispiele:', diff --git a/src/languages/en.ts b/src/languages/en.ts index 1aa45fedf9efe..2a124f3ab950c 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5226,7 +5226,8 @@ const translations = { }, timeTracking: { title: 'Time', - subtitle: 'Set an hourly billable rate for employees to get paid for their time.', + subtitle: 'Set a billable hourly rate for time tracking.', + defaultHourlyRate: 'Default hourly rate', }, }, reports: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 0bef8e38094e5..e9b4c09bf9c35 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4981,7 +4981,8 @@ ${amount} para ${merchant} - ${date}`, }, timeTracking: { title: 'Tiempo', - subtitle: 'Establece una tarifa facturable por hora para que los empleados reciban pago por su tiempo.', + subtitle: 'Establecer una tarifa por hora facturable para el seguimiento de tiempo.', + defaultHourlyRate: 'Tarifa por hora predeterminada', }, }, reports: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 21e9aa2a2cbfc..b3c0dd9a9a7cc 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5313,7 +5313,11 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. title: 'Règles', subtitle: 'Exigez des reçus, signalez les dépenses élevées, et plus encore.', }, - timeTracking: {title: 'Heure', subtitle: 'Définissez un taux horaire facturable pour que les employés soient rémunérés pour leur temps.'}, + timeTracking: { + title: 'Heure', + subtitle: 'Définissez un taux horaire facturable pour le suivi du temps.', + defaultHourlyRate: 'Taux horaire par défaut', + }, }, reports: { reportsCustomTitleExamples: 'Exemples :', diff --git a/src/languages/it.ts b/src/languages/it.ts index 7b2269347d99d..e7f7b22e32126 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5290,7 +5290,11 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. title: 'Regole', subtitle: 'Richiedi ricevute, segnala spese elevate e altro ancora.', }, - timeTracking: {title: 'Ora', subtitle: 'Imposta una tariffa oraria fatturabile per consentire ai dipendenti di essere pagati per il loro tempo.'}, + timeTracking: { + title: 'Ora', + subtitle: 'Imposta una tariffa oraria fatturabile per il monitoraggio del tempo.', + defaultHourlyRate: 'Tariffa oraria predefinita', + }, }, reports: { reportsCustomTitleExamples: 'Esempi:', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c506acd9987fc..f2a7a43a178d2 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5254,7 +5254,11 @@ _より詳しい手順については、[ヘルプサイトをご覧ください title: 'ルール', subtitle: 'レシートの必須化や高額支出のフラグ付けなどを設定できます。', }, - timeTracking: {title: '時間', subtitle: '従業員が作業時間に対して支払いを受けられるよう、時間単位の請求レートを設定します。'}, + timeTracking: { + title: '時間', + subtitle: 'タイムトラッキング用の時間単価請求レートを設定します。', + defaultHourlyRate: 'デフォルトの時間単価', + }, }, reports: { reportsCustomTitleExamples: '例:', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 83f7e09f092fb..e8e128683f655 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5280,7 +5280,11 @@ _Voor gedetailleerdere instructies, [bezoek onze helpsite](${CONST.NETSUITE_IMPO title: 'Regels', subtitle: 'Vereis bonnetjes, markeer hoge uitgaven en meer.', }, - timeTracking: {title: 'Tijd', subtitle: 'Stel een uurtarief in waarmee medewerkers worden betaald voor hun tijd.'}, + timeTracking: { + title: 'Tijd', + subtitle: 'Stel een factureerbaar uurtarief in voor tijdregistratie.', + defaultHourlyRate: 'Standaard uurtarief', + }, }, reports: { reportsCustomTitleExamples: 'Voorbeelden:', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index f151618206c1e..7205a50e84dc1 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5272,7 +5272,11 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy title: 'Zasady', subtitle: 'Wymagaj paragonów, oznaczaj wysokie wydatki i nie tylko.', }, - timeTracking: {title: 'Czas', subtitle: 'Ustaw godzinową stawkę rozliczeniową, aby pracownicy byli wynagradzani za swój czas.'}, + timeTracking: { + title: 'Czas', + subtitle: 'Ustaw godzinową stawkę rozliczeniową do śledzenia czasu.', + defaultHourlyRate: 'Domyślna stawka godzinowa', + }, }, reports: { reportsCustomTitleExamples: 'Przykłady:', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 91fa128294bd6..d2c8d0c6a3bbd 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5271,7 +5271,11 @@ _Para instruções mais detalhadas, [visite nosso site de ajuda](${CONST.NETSUIT title: 'Regras', subtitle: 'Exigir recibos, sinalizar gastos elevados e muito mais.', }, - timeTracking: {title: 'Hora', subtitle: 'Defina uma taxa horária faturável para que os funcionários sejam pagos pelo tempo trabalhado.'}, + timeTracking: { + title: 'Hora', + subtitle: 'Defina uma taxa horária faturável para o controle de tempo.', + defaultHourlyRate: 'Taxa horária padrão', + }, }, reports: { reportsCustomTitleExamples: 'Exemplos:', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 6d33440278002..f525cb5d5b333 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5164,7 +5164,11 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM title: '规则', subtitle: '要求收据、标记高额支出等。', }, - timeTracking: {title: '时间', subtitle: '为员工设置按小时计费的费率,以便根据他们的工作时间获得报酬。'}, + timeTracking: { + title: '时间', + subtitle: '为时间跟踪设置按小时计费费率。', + defaultHourlyRate: '默认时薪', + }, }, reports: { reportsCustomTitleExamples: '示例:', diff --git a/src/libs/API/parameters/SetPolicyTimeTrackingDefaultRateParams.ts b/src/libs/API/parameters/SetPolicyTimeTrackingDefaultRateParams.ts new file mode 100644 index 0000000000000..9703fb203e932 --- /dev/null +++ b/src/libs/API/parameters/SetPolicyTimeTrackingDefaultRateParams.ts @@ -0,0 +1,8 @@ +type SetPolicyTimeTrackingDefaultRateParams = { + policyID: string; + + /** Default hourly rate in policy's currency units (not subunits). */ + defaultRate: number; +}; + +export default SetPolicyTimeTrackingDefaultRateParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index b409eb168ffe5..d498f0970fad5 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -471,3 +471,4 @@ export type {default as SetPolicyCodingRuleParams} from './SetPolicyCodingRulePa export type {default as RegisterAuthenticationKeyParams} from './RegisterAuthenticationKeyParams'; export type {default as TroubleshootMultifactorAuthenticationParams} from './TroubleshootMultifactorAuthenticationParams'; export type {default as RequestAuthenticationChallengeParams} from './RequestAuthenticationChallengeParams'; +export type {default as SetPolicyTimeTrackingDefaultRateParams} from './SetPolicyTimeTrackingDefaultRateParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c0682c87fede7..883aed56d3051 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -253,6 +253,7 @@ const WRITE_COMMANDS = { ENABLE_POLICY_COMPANY_CARDS: 'EnablePolicyCompanyCards', ENABLE_POLICY_INVOICING: 'EnablePolicyInvoicing', ENABLE_POLICY_TIME_TRACKING: 'EnablePolicyTimeTracking', + SET_POLICY_TIME_TRACKING_DEFAULT_RATE: 'SetPolicyTimeTrackingDefaultRate', SET_POLICY_RULES_ENABLED: 'SetPolicyRulesEnabled', SET_POLICY_CODING_RULE: 'SetPolicyCodingRule', SET_POLICY_EXPENSE_MAX_AMOUNT_NO_RECEIPT: 'SetPolicyExpenseMaxAmountNoReceipt', @@ -892,6 +893,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_DISTANCE_TAX_CLAIMABLE_VALUE]: Parameters.UpdatePolicyDistanceRateValueParams; [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED]: Parameters.SetPolicyDistanceRatesEnabledParams; [WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES]: Parameters.DeletePolicyDistanceRatesParams; + [WRITE_COMMANDS.SET_POLICY_TIME_TRACKING_DEFAULT_RATE]: Parameters.SetPolicyTimeTrackingDefaultRateParams; [WRITE_COMMANDS.DISMISS_TRACK_EXPENSE_ACTIONABLE_WHISPER]: Parameters.DismissTrackExpenseActionableWhisperParams; [WRITE_COMMANDS.UPDATE_BILLING_CARD_CURRENCY]: Parameters.UpdateBillingCurrencyParams; [WRITE_COMMANDS.CONVERT_TRACKED_EXPENSE_TO_REQUEST]: Parameters.ConvertTrackedExpenseToRequestParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 6d7854bf8088e..ebf7eef19595e 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -844,6 +844,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/receiptPartners/InviteReceiptPartnerPolicyPage').default, [SCREENS.WORKSPACE.RECEIPT_PARTNERS_INVITE_EDIT]: () => require('../../../../pages/workspace/receiptPartners/EditInviteReceiptPartnerPolicyPage').default, [SCREENS.WORKSPACE.RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT]: () => require('../../../../pages/workspace/receiptPartners/ChangeReceiptBillingAccountPage').default, + [SCREENS.WORKSPACE.TIME_TRACKING_DEFAULT_RATE]: () => require('../../../../pages/workspace/timeTracking/WorkspaceTimeTrackingDefaultRatePage').default, [SCREENS.DOMAIN.VERIFY]: () => require('../../../../pages/domain/SamlVerifyDomainPage').default, [SCREENS.DOMAIN.VERIFIED]: () => require('../../../../pages/domain/SamlDomainVerifiedPage').default, [SCREENS.DOMAIN.ADMIN_DETAILS]: () => require('../../../../pages/domain/Admins/DomainAdminDetailsPage').default, diff --git a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx index 99a11121f25b4..2345747303a14 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx @@ -34,6 +34,7 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default, [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../../pages/workspace/travel/PolicyTravelPage').default, [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPage').default, + [SCREENS.WORKSPACE.TIME_TRACKING]: () => require('../../../../pages/workspace/timeTracking/WorkspaceTimeTrackingPage').default, } satisfies Screens; const Split = createSplitNavigator(); diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 872118eb179b0..5dce6634d75e7 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -311,6 +311,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.PER_DIEM_EDIT_CURRENCY]: { path: ROUTES.WORKSPACE_PER_DIEM_EDIT_CURRENCY.route, }, + [SCREENS.WORKSPACE.TIME_TRACKING_DEFAULT_RATE]: { + path: ROUTES.WORKSPACE_TIME_TRACKING_DEFAULT_RATE.route, + }, [SCREENS.DOMAIN.VERIFY]: { path: ROUTES.DOMAIN_VERIFY.route, }, @@ -2108,6 +2111,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.RULES]: { path: ROUTES.WORKSPACE_RULES.route, }, + [SCREENS.WORKSPACE.TIME_TRACKING]: { + path: ROUTES.WORKSPACE_TIME_TRACKING.route, + }, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index c94193d3a7492..6694e10c25390 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1471,6 +1471,9 @@ type SettingsNavigatorParamList = { rateID: string; subRateID: string; }; + [SCREENS.WORKSPACE.TIME_TRACKING_DEFAULT_RATE]: { + policyID: string; + }; [SCREENS.DOMAIN.VERIFY]: { domainAccountID: number; }; @@ -2640,6 +2643,9 @@ type WorkspaceSplitNavigatorParamList = { [SCREENS.WORKSPACE.RULES]: { policyID: string; }; + [SCREENS.WORKSPACE.TIME_TRACKING]: { + policyID: string; + }; }; type DomainSplitNavigatorParamList = { diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 694840927bb64..da8bced1f5dcb 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1779,8 +1779,8 @@ function isTimeTrackingEnabled(policy: OnyxEntry): boolean { return !!policy?.units?.time?.enabled; } -function getDefaultTimeTrackingRate(policy: Policy): number | undefined { - return policy.units?.time?.rate ? convertToBackendAmount(policy.units?.time?.rate) : undefined; +function getDefaultTimeTrackingRate(policy: Partial>): number | undefined { + return policy?.units?.time?.rate ? convertToBackendAmount(policy.units.time.rate) : undefined; } export { diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 3ada3bfd7bd2b..e2d4aca81a829 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -4809,6 +4809,44 @@ function enablePolicyTimeTracking(policyID: string, enabled: boolean) { } } +/** + * Update policy's time tracking default hourly rate. + * The rate should be passed in currency units, not subunits. + */ +function setPolicyTimeTrackingDefaultRate(policyID: string, rate: number) { + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + units: { + time: { + rate, + }, + }, + pendingFields: { + timeTrackingDefaultRate: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ], + finallyData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingFields: { + timeTrackingDefaultRate: null, + }, + }, + }, + ], + }; + + API.write(WRITE_COMMANDS.SET_POLICY_TIME_TRACKING_DEFAULT_RATE, {policyID, defaultRate: rate}, onyxData); +} + function openPolicyMoreFeaturesPage(policyID: string) { const params: OpenPolicyMoreFeaturesPageParams = {policyID}; @@ -6892,4 +6930,5 @@ export { inviteWorkspaceEmployeesToUber, setWorkspaceConfirmationCurrency, setPolicyRequireCompanyCardsEnabled, + setPolicyTimeTrackingDefaultRate, }; diff --git a/src/pages/iou/request/step/IOURequestStepHours.tsx b/src/pages/iou/request/step/IOURequestStepHours.tsx index df9a527a9f2b3..d88be50cf8460 100644 --- a/src/pages/iou/request/step/IOURequestStepHours.tsx +++ b/src/pages/iou/request/step/IOURequestStepHours.tsx @@ -53,7 +53,7 @@ function IOURequestStepHours({ const {accountID} = useCurrentUserPersonalDetails(); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: true}); const currency = policy?.outputCurrency ?? CONST.CURRENCY.USD; - const defaultPolicyRate = policy ? getDefaultTimeTrackingRate(policy) : undefined; + const defaultPolicyRate = getDefaultTimeTrackingRate(policy); const rate = transaction?.comment?.units?.rate ?? defaultPolicyRate; const {translate} = useLocalize(); diff --git a/src/pages/iou/request/step/IOURequestStepTimeWorkspace.tsx b/src/pages/iou/request/step/IOURequestStepTimeWorkspace.tsx index 4d726c798ab72..825fab3a1e6cf 100644 --- a/src/pages/iou/request/step/IOURequestStepTimeWorkspace.tsx +++ b/src/pages/iou/request/step/IOURequestStepTimeWorkspace.tsx @@ -37,7 +37,7 @@ function IOURequestStepTimeWorkspace({route, navigation}: IOURequestStepTimeWork setTransactionReport(transactionID, {reportID: policyExpenseChat.reportID}, isTransactionDraft); setMoneyRequestParticipantsFromReport(transactionID, policyExpenseChat, accountID); - const defaultRate = policy ? getDefaultTimeTrackingRate(policy) : undefined; + const defaultRate = getDefaultTimeTrackingRate(policy); if (defaultRate) { setMoneyRequestTimeRate(transactionID, defaultRate, isTransactionDraft); } diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index eeec6cd90e89b..6967b25934eb8 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -40,6 +40,7 @@ import { isPendingDeletePolicy, isPolicyAdmin, isPolicyFeatureEnabled, + isTimeTrackingEnabled, shouldShowEmployeeListError, shouldShowSyncError, shouldShowTaxRateError, @@ -108,6 +109,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac 'Users', 'Workflows', 'LuggageWithLines', + 'Clock', ] as const); const policy = policyDraft?.id ? policyDraft : policyProp; @@ -130,6 +132,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const wasRendered = useRef(false); const prevPendingFields = usePrevious(policy?.pendingFields); const shouldDisplayLHB = !shouldUseNarrowLayout; + const isPolicyTimeTrackingEnabled = isTimeTrackingEnabled(policy); const policyFeatureStates = useMemo( () => ({ [CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED]: policy?.areDistanceRatesEnabled, @@ -146,6 +149,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac [CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_RATES_ENABLED]: policy?.arePerDiemRatesEnabled, [CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED]: isUberForBusinessEnabled && (policy?.receiptPartners?.enabled ?? false), [CONST.POLICY.MORE_FEATURES.IS_TRAVEL_ENABLED]: policy?.isTravelEnabled, + [CONST.POLICY.MORE_FEATURES.IS_TIME_TRACKING_ENABLED]: isPolicyTimeTrackingEnabled, }), [ policy?.areDistanceRatesEnabled, @@ -164,6 +168,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac policy?.receiptPartners?.enabled, isUberForBusinessEnabled, policy?.isTravelEnabled, + isPolicyTimeTrackingEnabled, ], ) as PolicyFeatureStates; @@ -341,6 +346,16 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac }); } + if (isBetaEnabled(CONST.BETAS.TIME_TRACKING) && featureStates?.[CONST.POLICY.MORE_FEATURES.IS_TIME_TRACKING_ENABLED]) { + protectedMenuItems.push({ + translationKey: 'iou.time', + icon: expensifyIcons.Clock, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TIME_TRACKING.getRoute(policyID)))), + screenName: SCREENS.WORKSPACE.TIME_TRACKING, + highlighted: highlightedFeature === CONST.POLICY.MORE_FEATURES.IS_TIME_TRACKING_ENABLED, + }); + } + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_INVOICES_ENABLED]) { const currencyCode = policy?.outputCurrency ?? CONST.CURRENCY.USD; protectedMenuItems.push({ @@ -397,6 +412,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac expensifyIcons.CreditCard, expensifyIcons.CalendarSolid, expensifyIcons.InvoiceGeneric, + expensifyIcons.Clock, singleExecution, waitForNavigate, featureStates, @@ -410,6 +426,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac shouldShowEnterCredentialsError, hasPolicyCategoryError, shouldShowRBR, + isBetaEnabled, ]); // We only update feature states if they aren't pending. diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 4ae303a34225e..2376e6f94a6bd 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -272,7 +272,10 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro enablePolicyTimeTracking(policyID, isEnabled); }, onPress: () => { - // TODO: Navigate to the Time Tracking settings page when implemented. + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_TIME_TRACKING.getRoute(policyID)); }, }); } diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 75a28c0f3f779..77585ce1e9248 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -92,6 +92,9 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps & /** Content to be added as modal */ modals?: ReactNode; + + /** Whether to use the maxHeight (true) or use the 100% of the height (false) */ + shouldEnableMaxHeight?: boolean; }; function fetchData(policyID: string | undefined, skipVBBACal?: boolean) { @@ -118,6 +121,7 @@ function WorkspacePageWithSections({ shouldShowLoading = true, shouldShowOfflineIndicatorInWideScreen = false, shouldShowNonAdmin = false, + shouldEnableMaxHeight = true, headerContent, testID, shouldShowNotFoundPage = false, @@ -200,7 +204,7 @@ function WorkspacePageWithSections({ ; + +function WorkspaceTimeTrackingDefaultRatePage({ + route: { + params: {policyID}, + }, +}: WorkspaceTimeTrackingDefaultRatePageProps) { + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const styles = useThemeStyles(); + const [policy, policyFetchStatus] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: true, selector: policyTimeTrackingSelector}); + const currency = policy?.outputCurrency ?? CONST.CURRENCY.USD; + + if (!policy || isLoadingOnyxValue(policyFetchStatus)) { + return ; + } + + return ( + + + + { + setPolicyTimeTrackingDefaultRate(policyID, Number.parseFloat(values[INPUT_IDS.RATE])); + Navigation.dismissModal(); + }} + style={[styles.flex1, styles.mh5]} + enabledWhenOffline + validate={(values) => getFieldRequiredErrors(values, [INPUT_IDS.RATE])} + > + + + + + ); +} + +export default WorkspaceTimeTrackingDefaultRatePage; diff --git a/src/pages/workspace/timeTracking/WorkspaceTimeTrackingDefaultRateSection.tsx b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingDefaultRateSection.tsx new file mode 100644 index 0000000000000..f13a95616cc66 --- /dev/null +++ b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingDefaultRateSection.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import Section from '@components/Section'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {convertAmountToDisplayString} from '@libs/CurrencyUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {getDefaultTimeTrackingRate} from '@libs/PolicyUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import {policyTimeTrackingSelector} from '@src/selectors/Policy'; + +function WorkspaceTimeTrackingDefaultRateSection({policyID}: {policyID: string}) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + canBeMissing: true, + selector: policyTimeTrackingSelector, + }); + + return ( +
+ + Navigation.navigate(ROUTES.WORKSPACE_TIME_TRACKING_DEFAULT_RATE.getRoute(policyID))} + style={styles.sectionMenuItemTopDescription} + /> + +
+ ); +} + +export default WorkspaceTimeTrackingDefaultRateSection; diff --git a/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx new file mode 100644 index 0000000000000..189c26dd3a159 --- /dev/null +++ b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; +import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; +import WorkspaceTimeTrackingDefaultRateSection from './WorkspaceTimeTrackingDefaultRateSection'; + +type WorkspaceTimeTrackingPageProps = PlatformStackScreenProps; + +function WorkspaceTimeTrackingPage({route}: WorkspaceTimeTrackingPageProps) { + const {policyID} = route.params; + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const illustrations = useMemoizedLazyIllustrations(['Clock']); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {isBetaEnabled} = usePermissions(); + + return ( + + + + + + + + ); +} + +export default WorkspaceTimeTrackingPage; diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index fb5929e1c2966..b847b6b40f001 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -53,7 +53,8 @@ type PolicyRouteName = | typeof SCREENS.WORKSPACE.RULES | typeof SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW | typeof SCREENS.WORKSPACE.COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION - | typeof SCREENS.WORKSPACE.ACCOUNTING.CLAIM_OFFER; + | typeof SCREENS.WORKSPACE.ACCOUNTING.CLAIM_OFFER + | typeof SCREENS.WORKSPACE.TIME_TRACKING; type PolicyRoute = PlatformStackRouteProp; diff --git a/src/selectors/Policy.ts b/src/selectors/Policy.ts index 3361cd89bee04..a0230b8666326 100644 --- a/src/selectors/Policy.ts +++ b/src/selectors/Policy.ts @@ -37,4 +37,13 @@ const createAllPolicyReportFieldsSelector = (policies: OnyxCollection, l return Object.fromEntries(nonFormulaReportFields); }; -export {activePolicySelector, createPoliciesSelector, createAllPolicyReportFieldsSelector, ownerPoliciesSelector, activeAdminPoliciesSelector}; +const policyTimeTrackingSelector = (policy: OnyxEntry) => + policy && { + outputCurrency: policy.outputCurrency, + pendingFields: { + timeTrackingDefaultRate: policy.pendingFields?.timeTrackingDefaultRate, + }, + units: policy.units, + }; + +export {activePolicySelector, createPoliciesSelector, createAllPolicyReportFieldsSelector, ownerPoliciesSelector, activeAdminPoliciesSelector, policyTimeTrackingSelector}; diff --git a/src/types/form/WorkspaceTimeTrackingDefaultRateForm.ts b/src/types/form/WorkspaceTimeTrackingDefaultRateForm.ts new file mode 100644 index 0000000000000..ecb2da919422f --- /dev/null +++ b/src/types/form/WorkspaceTimeTrackingDefaultRateForm.ts @@ -0,0 +1,18 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + RATE: 'rate', +} as const; + +type InputID = ValueOf; + +type WorkspaceTimeTrackingDefaultRateForm = Form< + InputID, + { + [INPUT_IDS.RATE]: string; + } +>; + +export type {WorkspaceTimeTrackingDefaultRateForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index cac1d914cea61..0110061c49189 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -106,3 +106,4 @@ export type {ResetDomainForm} from './ResetDomainForm'; export type {ExpenseRuleForm} from './ExpenseRuleForm'; export type {MerchantRuleForm} from './MerchantRuleForm'; export type {AddDomainMemberForm} from './AddDomainMemberForm'; +export type {WorkspaceTimeTrackingDefaultRateForm} from './WorkspaceTimeTrackingDefaultRateForm'; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 067fbe82fa384..60f4e624d19cb 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -2090,7 +2090,7 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the policy requires purchases to be on a company card */ requireCompanyCardsEnabled?: boolean; } & Partial, - 'addWorkspaceRoom' | keyof ACHAccount | keyof Attributes | 'isTimeTrackingEnabled' + 'addWorkspaceRoom' | keyof ACHAccount | keyof Attributes | 'isTimeTrackingEnabled' | 'timeTrackingDefaultRate' >; /** Stages of policy connection sync */