[Behind Time Tracking Beta] Create time expenses in workspace reports#78137
Conversation
|
Hey, I noticed you changed If you want to automatically generate translations for other locales, an Expensify employee will have to:
Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running: npx ts-node ./scripts/generateTranslations.ts --helpTypically, you'd want to translate only what you changed by running |
Codecov Report❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.
|
This comment has been minimized.
This comment has been minimized.
|
Known backend issues that should be resolved soon:
|
|
@DylanDylann Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
| }, | ||
| }, | ||
| ); | ||
| const shouldShowTimeOption = isBetaEnabled(CONST.BETAS.TIME_TRACKING) && iouType === CONST.IOU.TYPE.SUBMIT && !isFromGlobalCreate && hasCurrentPolicyTimeTrackingEnabled; |
There was a problem hiding this comment.
we still allow create time expense via FAB button. Confirmed here: https://expensify.slack.com/archives/C07NMDKEFMH/p1767692133538669?thread_ts=1767663557.100389&cid=C07NMDKEFMH
| const shouldShowTimeOption = isBetaEnabled(CONST.BETAS.TIME_TRACKING) && iouType === CONST.IOU.TYPE.SUBMIT && !isFromGlobalCreate && hasCurrentPolicyTimeTrackingEnabled; | |
| const shouldShowTimeOption = isBetaEnabled(CONST.BETAS.TIME_TRACKING) && iouType === CONST.IOU.TYPE.SUBMIT && hasCurrentPolicyTimeTrackingEnabled; |
There was a problem hiding this comment.
this will be implemented as part of this issue: #77684
the issue for creating time expenses was divided into 3 subissues and there will most likely be one PR per each. this is the first one. editing Hour and Rate in the confirmation page is part of this one: #77683 |
|
🚧 @grgia has triggered a test Expensify/App build. You can view the workflow run here. |
🦜 Polyglot Parrot! 🦜Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues: View the translation diffdiff --git a/src/languages/de.ts b/src/languages/de.ts
index 1c715b47..a822e2a8 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -1134,7 +1134,7 @@ const translations: TranslationDeepObject<typeof en> = {
movedFromReport: ({reportName}: MovedFromReportParams) => `hat eine Ausgabe verschoben${reportName ? `von ${reportName}` : ''}`,
movedTransactionTo: ({reportUrl, reportName}: MovedTransactionParams) => `hat diese Ausgabe verschoben${reportName ? `zu <a href="${reportUrl}">${reportName}</a>` : ''}`,
movedTransactionFrom: ({reportUrl, reportName}: MovedTransactionParams) => `hat diese Ausgabe verschoben${reportName ? `von <a href="${reportUrl}">${reportName}</a>` : ''}`,
- movedUnreportedTransaction: ({reportUrl}: MovedTransactionParams) => `hat diese Ausgabe aus deinem <a href="${reportUrl}">Persönlichen Bereich</a> verschoben`,
+ movedUnreportedTransaction: ({reportUrl}: MovedTransactionParams) => `diese Ausgabe aus deinem <a href="${reportUrl}">persönlichen Bereich</a> verschoben`,
unreportedTransaction: ({reportUrl}: MovedTransactionParams) => `hat diese Ausgabe in deinen <a href="${reportUrl}">persönlichen Bereich</a> verschoben`,
movedAction: ({shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName}: MovedActionParams) => {
if (shouldHideMovedReportUrl) {
@@ -1465,7 +1465,7 @@ const translations: TranslationDeepObject<typeof en> = {
splitDateRange: ({startDate, endDate, count}: SplitDateRangeParams) => `${startDate} bis ${endDate} (${count} Tage)`,
splitByDate: 'Nach Datum aufteilen',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `bericht aufgrund eines benutzerdefinierten Genehmigungsworkflows an ${to} weitergeleitet`,
- timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} Stunden @ ${rate} / Stunde`, hrs: 'Std.'},
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? 'Stunde' : 'Stunden'} @ ${rate} / Stunde`, hrs: 'Std.'},
},
transactionMerge: {
listPage: {
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 7b3bb8a1..26018538 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -1466,7 +1466,7 @@ const translations: TranslationDeepObject<typeof en> = {
splitDateRange: ({startDate, endDate, count}: SplitDateRangeParams) => `Du ${startDate} au ${endDate} (${count} jours)`,
splitByDate: 'Scinder par date',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `rapport acheminé vers ${to} en raison d'un workflow d'approbation personnalisé`,
- timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} heures @ ${rate} / heure`, hrs: 'h'},
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? 'heure' : 'heures'} @ ${rate} / heure`, hrs: 'h'},
},
transactionMerge: {
listPage: {
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 7772bec0..2e6932e4 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -1460,7 +1460,7 @@ const translations: TranslationDeepObject<typeof en> = {
splitDateRange: ({startDate, endDate, count}: SplitDateRangeParams) => `${startDate} a ${endDate} (${count} giorni)`,
splitByDate: 'Dividi per data',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `rapporto inoltrato a ${to} a causa del flusso di lavoro di approvazione personalizzato`,
- timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ore @ ${rate} / ora`, hrs: 'ore'},
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? 'ora' : 'ore'} @ ${rate} / ora`, hrs: 'ore'},
},
transactionMerge: {
listPage: {
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 0145febd..0c8a981c 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -1130,7 +1130,7 @@ const translations: TranslationDeepObject<typeof en> = {
movedFromReport: ({reportName}: MovedFromReportParams) => `経費を移動しました${reportName ? `${reportName} から` : ''}`,
movedTransactionTo: ({reportUrl, reportName}: MovedTransactionParams) => `この経費を移動しました${reportName ? `<a href="${reportUrl}">${reportName}</a> へ` : ''}`,
movedTransactionFrom: ({reportUrl, reportName}: MovedTransactionParams) => `この経費を移動しました${reportName ? `<a href="${reportUrl}">${reportName}</a> から` : ''}`,
- movedUnreportedTransaction: ({reportUrl}: MovedTransactionParams) => `この経費を<a href="${reportUrl}">個人スペース</a>から移動しました`,
+ movedUnreportedTransaction: ({reportUrl}: MovedTransactionParams) => `この経費を<a href="${reportUrl}">パーソナルスペース</a>から移動しました`,
unreportedTransaction: ({reportUrl}: MovedTransactionParams) => `この経費はあなたの<a href="${reportUrl}">パーソナルスペース</a>に移動されました`,
movedAction: ({shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName}: MovedActionParams) => {
if (shouldHideMovedReportUrl) {
@@ -1459,7 +1459,7 @@ const translations: TranslationDeepObject<typeof en> = {
splitDateRange: ({startDate, endDate, count}: SplitDateRangeParams) => `${startDate} から ${endDate} まで(${count} 日間)`,
splitByDate: '日付で分割',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `カスタム承認ワークフローにより、${to} 宛にルーティングされたレポート`,
- timeTracking: {hoursAt: (hours: number, rate: string) => `${hours}時間 @ ${rate} / 時間`, hrs: '時間'},
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? '時間' : '時間'} @ ${rate} / 時間`, hrs: '時間'},
},
transactionMerge: {
listPage: {
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index ca8adb50..ca617983 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -1459,7 +1459,7 @@ const translations: TranslationDeepObject<typeof en> = {
splitDateRange: ({startDate, endDate, count}: SplitDateRangeParams) => `${startDate} tot ${endDate} (${count} dagen)`,
splitByDate: 'Splitsen op datum',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `rapport doorgestuurd naar ${to} vanwege aangepaste goedkeuringsworkflow`,
- timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} uur @ ${rate} / uur`, hrs: 'uur'},
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? 'uur' : 'uren'} @ ${rate} / uur`, hrs: 'uur'},
},
transactionMerge: {
listPage: {
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index aead0cbc..06681c81 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -1457,7 +1457,7 @@ const translations: TranslationDeepObject<typeof en> = {
splitDateRange: ({startDate, endDate, count}: SplitDateRangeParams) => `${startDate} do ${endDate} (${count} dni)`,
splitByDate: 'Podziel według daty',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `raport przekazany do ${to} z powodu niestandardowego procesu zatwierdzania`,
- timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} godzin @ ${rate} / godzinę`, hrs: 'godz.'},
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? 'godzina' : 'godziny'} @ ${rate} / godzinę`, hrs: 'godz.'},
},
transactionMerge: {
listPage: {
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 62e4c133..4194d51e 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -1456,7 +1456,7 @@ const translations: TranslationDeepObject<typeof en> = {
splitDateRange: ({startDate, endDate, count}: SplitDateRangeParams) => `${startDate} a ${endDate} (${count} dias)`,
splitByDate: 'Dividir por data',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `relatório encaminhado para ${to} devido ao fluxo de trabalho de aprovação personalizado`,
- timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} horas @ ${rate} / hora`, hrs: 'h'},
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? 'hora' : 'horas'} @ ${rate} / hora`, hrs: 'h'},
},
transactionMerge: {
listPage: {
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 597db2d0..d6412547 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -1114,7 +1114,7 @@ const translations: TranslationDeepObject<typeof en> = {
movedFromReport: ({reportName}: MovedFromReportParams) => `移动了一笔报销${reportName ? `来自 ${reportName}` : ''}`,
movedTransactionTo: ({reportUrl, reportName}: MovedTransactionParams) => `已移动此报销${reportName ? `到 <a href="${reportUrl}">${reportName}</a>` : ''}`,
movedTransactionFrom: ({reportUrl, reportName}: MovedTransactionParams) => `已移动此报销${reportName ? `来自 <a href="${reportUrl}">${reportName}</a>` : ''}`,
- movedUnreportedTransaction: ({reportUrl}: MovedTransactionParams) => `已将此费用从您的<a href="${reportUrl}">个人空间</a>中移出`,
+ movedUnreportedTransaction: ({reportUrl}: MovedTransactionParams) => `已将此报销从你的<a href="${reportUrl}">个人空间</a>移动`,
unreportedTransaction: ({reportUrl}: MovedTransactionParams) => `已将此报销移动到你的<a href="${reportUrl}">个人空间</a>`,
movedAction: ({shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName}: MovedActionParams) => {
if (shouldHideMovedReportUrl) {
@@ -1434,7 +1434,7 @@ const translations: TranslationDeepObject<typeof en> = {
splitDateRange: ({startDate, endDate, count}: SplitDateRangeParams) => `${startDate} 至 ${endDate}(${count} 天)`,
splitByDate: '按日期拆分',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `报告因自定义审批工作流而转发至 ${to}`,
- timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} 小时 @ ${rate} / 小时`, hrs: '小时'},
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? '小时' : '小时'} @ ${rate} / 小时`, hrs: '小时'},
},
transactionMerge: {
listPage: {
Note You can apply these changes to your branch by copying the patch to your clipboard, then running |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
@mhawryluk I can reproduce this bug consistently, these are the detailed steps
Screen.Recording.2026-01-13.at.12.09.17.mov |
|
After a quick check, I found that resetIOUTypeIfChanged is called in IOURequestStartPage.tsx after the count is set, which resets the transaction data |
Screen.Recording.2026-01-13.at.18.30.10.mov |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppScreen.Recording.2026-01-13.at.19.30.32.movAndroid: mWeb ChromeScreen.Recording.2026-01-13.at.19.26.22.moviOS: HybridAppScreen.Recording.2026-01-13.at.19.25.00.moviOS: mWeb SafariScreen.Recording.2026-01-13.at.19.24.27.movMacOS: Chrome / SafariUploading Screen Recording 2026-01-13 at 19.25.55.mov… |
grgia
left a comment
There was a problem hiding this comment.
quick question about this one. Would you fix conflicts as well?
| function isTimeRequest(transaction: OnyxEntry<Transaction>): boolean { | ||
| // This is used during the expense creation flow before the transaction has been saved to the server | ||
| if (lodashHas(transaction, 'iouRequestType')) { | ||
| return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.TIME; | ||
| } | ||
|
|
||
| // This is the case for transaction objects once they have been saved to the server | ||
| return transaction?.comment?.type === CONST.TRANSACTION.TYPE.TIME; | ||
| } |
There was a problem hiding this comment.
should we favor the comment here?
There was a problem hiding this comment.
all the other types favor the iouRequestType. do you think there is a situation where this could be an issue?
|
Bug Logged #79459 |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚀 Deployed to staging by https://github.com/grgia in version: 9.3.1-0 🚀
|
|
🚀 Deployed to production by https://github.com/francoisl in version: 9.3.1-1 🚀
|


Explanation of Change
Allows creating a time expense in a workspace chat/report.
Fixed Issues
$ #77682
PROPOSAL: N/A
Tests
timeTrackingbeta enabled.Offline tests
Same as tests, but offline. The expense should be created optimistically, greyed out until back online.
QA Steps
Same as tests.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectioncanBeMissingparam foruseOnyxtoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Nagranie.z.ekranu.2025-12-23.o.13.23.10.mov
Android: mWeb Chrome
Nagranie.z.ekranu.2025-12-23.o.13.19.53.mov
iOS: Native
Simulator.Screen.Recording.-.iPhone.16.Pro.Max.-.2025-12-23.at.13.18.19.mp4
iOS: mWeb Safari
Simulator.Screen.Recording.-.iPhone.16.Pro.Max.-.2025-12-23.at.13.16.42.mp4
MacOS: Chrome / Safari
Nagranie.z.ekranu.2025-12-23.o.13.13.40.mov