@zeniai/client-epic-state 5.3.12-beta0DI → 5.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/lib/commonStateTypes/animations.d.ts +1 -1
  2. package/lib/commonStateTypes/animations.js +1 -0
  3. package/lib/entity/task/taskPayload.d.ts +0 -1
  4. package/lib/entity/task/taskPayload.js +0 -1
  5. package/lib/entity/task/taskReducer.d.ts +1 -4
  6. package/lib/entity/task/taskReducer.js +2 -19
  7. package/lib/entity/task/taskState.d.ts +0 -3
  8. package/lib/epic.d.ts +2 -4
  9. package/lib/epic.js +2 -9
  10. package/lib/esm/commonStateTypes/animations.js +1 -0
  11. package/lib/esm/entity/task/taskPayload.js +0 -1
  12. package/lib/esm/entity/task/taskReducer.js +1 -18
  13. package/lib/esm/epic.js +2 -9
  14. package/lib/esm/index.js +5 -8
  15. package/lib/esm/reducer.js +0 -6
  16. package/lib/esm/view/aiCfoView/aiCfoViewReducer.js +65 -3
  17. package/lib/esm/view/aiCfoView/aiCfoViewSelector.js +25 -0
  18. package/lib/esm/view/aiCfoView/epics/transcribeAudioEpic.js +60 -0
  19. package/lib/esm/view/expenseAutomationView/types/transactionGroup.js +10 -5
  20. package/lib/esm/view/taskManager/taskDetailView/taskDetailSelector.js +0 -3
  21. package/lib/esm/view/taskManager/taskListView/taskList.js +0 -6
  22. package/lib/esm/view/taskManager/taskListView/taskListFilterHelpers.js +0 -7
  23. package/lib/esm/view/taskManager/taskListView/taskListSelector.js +11 -27
  24. package/lib/esm/view/taskManager/taskListView/taskListViewHelpers.js +0 -37
  25. package/lib/esm/view/transactionDetail/createTransaction/createTransactionPayload.js +19 -3
  26. package/lib/esm/view/transactionDetail/createTransaction/createTransactionState.js +2 -0
  27. package/lib/index.d.ts +6 -10
  28. package/lib/index.js +28 -37
  29. package/lib/reducer.d.ts +0 -2
  30. package/lib/reducer.js +0 -6
  31. package/lib/rootStateTypes.d.ts +0 -4
  32. package/lib/view/aiCfoView/aiCfoViewPayload.d.ts +20 -0
  33. package/lib/view/aiCfoView/aiCfoViewReducer.d.ts +2 -2
  34. package/lib/view/aiCfoView/aiCfoViewReducer.js +66 -4
  35. package/lib/view/aiCfoView/aiCfoViewSelector.d.ts +8 -1
  36. package/lib/view/aiCfoView/aiCfoViewSelector.js +27 -0
  37. package/lib/view/aiCfoView/aiCfoViewState.d.ts +9 -0
  38. package/lib/view/aiCfoView/epics/transcribeAudioEpic.d.ts +19 -0
  39. package/lib/view/aiCfoView/epics/transcribeAudioEpic.js +64 -0
  40. package/lib/view/expenseAutomationView/payload/transactionCategorizationPayload.d.ts +2 -2
  41. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +2 -2
  42. package/lib/view/expenseAutomationView/types/transactionGroup.d.ts +6 -6
  43. package/lib/view/expenseAutomationView/types/transactionGroup.js +10 -5
  44. package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +2 -2
  45. package/lib/view/taskManager/taskDetailView/taskDetailSelector.d.ts +0 -4
  46. package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +0 -3
  47. package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
  48. package/lib/view/taskManager/taskListView/taskList.js +0 -6
  49. package/lib/view/taskManager/taskListView/taskListFilterHelpers.d.ts +0 -4
  50. package/lib/view/taskManager/taskListView/taskListFilterHelpers.js +1 -9
  51. package/lib/view/taskManager/taskListView/taskListSelector.d.ts +3 -6
  52. package/lib/view/taskManager/taskListView/taskListSelector.js +10 -26
  53. package/lib/view/taskManager/taskListView/taskListViewHelpers.d.ts +1 -1
  54. package/lib/view/taskManager/taskListView/taskListViewHelpers.js +0 -37
  55. package/lib/view/transactionDetail/createTransaction/createTransactionPayload.d.ts +6 -3
  56. package/lib/view/transactionDetail/createTransaction/createTransactionPayload.js +19 -3
  57. package/lib/view/transactionDetail/createTransaction/createTransactionState.d.ts +7 -2
  58. package/lib/view/transactionDetail/createTransaction/createTransactionState.js +2 -0
  59. package/lib/view/transactionDetail/transactionDetailReducer.d.ts +20 -20
  60. package/package.json +1 -1
  61. package/lib/entity/timeEntry/timeEntryPayload.d.ts +0 -26
  62. package/lib/entity/timeEntry/timeEntryPayload.js +0 -2
  63. package/lib/entity/timeEntry/timeEntryReducer.d.ts +0 -8
  64. package/lib/entity/timeEntry/timeEntryReducer.js +0 -76
  65. package/lib/entity/timeEntry/timeEntrySelector.d.ts +0 -10
  66. package/lib/entity/timeEntry/timeEntrySelector.js +0 -32
  67. package/lib/entity/timeEntry/timeEntryState.d.ts +0 -43
  68. package/lib/entity/timeEntry/timeEntryState.js +0 -13
  69. package/lib/esm/entity/timeEntry/timeEntryPayload.js +0 -1
  70. package/lib/esm/entity/timeEntry/timeEntryReducer.js +0 -70
  71. package/lib/esm/entity/timeEntry/timeEntrySelector.js +0 -23
  72. package/lib/esm/entity/timeEntry/timeEntryState.js +0 -8
  73. package/lib/esm/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +0 -24
  74. package/lib/esm/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +0 -73
  75. package/lib/esm/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +0 -77
  76. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewPayload.js +0 -1
  77. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewReducer.js +0 -160
  78. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewSelector.js +0 -41
  79. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewState.js +0 -1
  80. package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.d.ts +0 -18
  81. package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +0 -28
  82. package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.d.ts +0 -23
  83. package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +0 -78
  84. package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.d.ts +0 -23
  85. package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +0 -83
  86. package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.d.ts +0 -27
  87. package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.js +0 -2
  88. package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.d.ts +0 -48
  89. package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.js +0 -164
  90. package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.d.ts +0 -24
  91. package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.js +0 -46
  92. package/lib/view/taskManager/timeEntryView/timeEntryViewState.d.ts +0 -21
  93. package/lib/view/taskManager/timeEntryView/timeEntryViewState.js +0 -5
@@ -47,6 +47,8 @@ export const initialAiCfoViewState = {
47
47
  fetchChatSessionHistoryByChatSessionId: {},
48
48
  responseStateByChatSessionId: {},
49
49
  stopSubmitStateByChatSessionId: {},
50
+ transcriptionState: { fetchState: 'Not-Started', error: undefined },
51
+ transcribingChatSessionId: undefined,
50
52
  suggestedQuestionsByPageContext: {},
51
53
  skillDetail: {
52
54
  fetchState: 'Not-Started',
@@ -147,6 +149,19 @@ function closeSkillsBrowserState(draft) {
147
149
  draft.uiState.isSkillsBrowserOpen = false;
148
150
  clearOpenSkillState(draft);
149
151
  }
152
+ /**
153
+ * Clear a finished dictation attempt when the conversation moves on, so a failed one
154
+ * stops showing "Could not transcribe that recording" in every later chat. A running
155
+ * take keeps its state — the composer is showing a live "Transcribing…" for it.
156
+ */
157
+ function resetFinishedTranscription(draft) {
158
+ if (draft.transcriptionState.fetchState === 'In-Progress') {
159
+ return;
160
+ }
161
+ draft.transcriptionState.fetchState = 'Not-Started';
162
+ draft.transcriptionState.error = undefined;
163
+ draft.transcribingChatSessionId = undefined;
164
+ }
150
165
  const aiCfoView = createSlice({
151
166
  name: 'aiCfoView',
152
167
  initialState: initialAiCfoViewState,
@@ -154,6 +169,7 @@ const aiCfoView = createSlice({
154
169
  submitQuestion: {
155
170
  reducer(draft, action) {
156
171
  const { sessionId } = action.payload;
172
+ resetFinishedTranscription(draft);
157
173
  // A turn is being sent, so the chat body has to be what the user sees. Doing this
158
174
  // here rather than at each call site is deliberate: the composer, the sample
159
175
  // chips, Run, and the `/` menu all submit, and every one of them was a separate
@@ -299,6 +315,7 @@ const aiCfoView = createSlice({
299
315
  setSession(draft, action) {
300
316
  draft.uiState.currentChatSessionId = action.payload.sessionId;
301
317
  draft.uiState.agentId = action.payload.agentId;
318
+ resetFinishedTranscription(draft);
302
319
  // Picking a session means wanting to see that chat, so the browser cannot stay over
303
320
  // it. In the reducer because there are six dispatch sites across three separate
304
321
  // rails, and covering them one at a time missed half of them.
@@ -351,6 +368,7 @@ const aiCfoView = createSlice({
351
368
  createSessionAndSubmit: {
352
369
  reducer(draft, action) {
353
370
  draft.uiState.agentId = action.payload.agentId;
371
+ resetFinishedTranscription(draft);
354
372
  draft.createSessionAndSubmitState.fetchState = 'In-Progress';
355
373
  draft.createSessionAndSubmitState.error = undefined;
356
374
  },
@@ -551,7 +569,49 @@ const aiCfoView = createSlice({
551
569
  return { payload: { chatMessageId, error } };
552
570
  },
553
571
  },
572
+ transcribeAudio: {
573
+ reducer(draft, action) {
574
+ draft.transcriptionState.fetchState = 'In-Progress';
575
+ draft.transcriptionState.error = undefined;
576
+ draft.transcribingChatSessionId = action.payload.chatSessionId;
577
+ },
578
+ prepare(audio, language, chatSessionId) {
579
+ return { payload: { audio, chatSessionId, language } };
580
+ },
581
+ },
582
+ transcribeAudioSuccess: {
583
+ reducer(draft, action) {
584
+ draft.transcriptionState.fetchState = 'Completed';
585
+ draft.transcriptionState.error = undefined;
586
+ // The user moved to another session while this was in flight — dropping it
587
+ // is the only safe option, since appending would put their words in a
588
+ // conversation they did not dictate into.
589
+ if (action.payload.chatSessionId !== draft.uiState.currentChatSessionId) {
590
+ return;
591
+ }
592
+ const transcript = action.payload.transcript.trim();
593
+ if (transcript === '') {
594
+ // Silence, not a failure: leave the composer untouched so the user can
595
+ // simply speak again rather than hunting for what went wrong.
596
+ return;
597
+ }
598
+ // Append to whatever is in the box now rather than to a snapshot taken when
599
+ // recording began — a take produces exactly one result, so there is nothing
600
+ // to reconcile, and anything typed while transcribing survives.
601
+ const existing = draft.uiState.currentInput.trim();
602
+ draft.uiState.currentInput =
603
+ existing === '' ? transcript : `${existing} ${transcript}`;
604
+ },
605
+ prepare(transcript, chatSessionId) {
606
+ return { payload: { chatSessionId, transcript } };
607
+ },
608
+ },
609
+ transcribeAudioFailure(draft, action) {
610
+ draft.transcriptionState.fetchState = 'Error';
611
+ draft.transcriptionState.error = action.payload;
612
+ },
554
613
  clearCurrentSessionId(draft) {
614
+ resetFinishedTranscription(draft);
555
615
  // New Chat: the empty composer is the point, so the browser cannot stay over it.
556
616
  if (draft.uiState.isSkillsBrowserOpen === true) {
557
617
  closeSkillsBrowserState(draft);
@@ -835,7 +895,8 @@ const aiCfoView = createSlice({
835
895
  // The server has it. Nothing it sends from here on will contain this row,
836
896
  // so the tombstone has done its job and holding it any longer is what
837
897
  // makes a routine recreated under the same id invisible forever.
838
- draft.routines.deletedScheduleIds = draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
898
+ draft.routines.deletedScheduleIds =
899
+ draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
839
900
  draft.routineMutation = {
840
901
  fetchState: 'Completed',
841
902
  error: undefined,
@@ -847,7 +908,8 @@ const aiCfoView = createSlice({
847
908
  // is what lets the refetch put the row back — leaving it hidden would
848
909
  // tell the user they had stopped something they had not, which is the
849
910
  // worst outcome this list can produce.
850
- draft.routines.deletedScheduleIds = draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
911
+ draft.routines.deletedScheduleIds =
912
+ draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
851
913
  // Its own slice: the refetch that puts the row back would otherwise wipe
852
914
  // this error, leaving the row silently reappearing with no explanation.
853
915
  draft.routineMutation = {
@@ -1183,5 +1245,5 @@ const aiCfoView = createSlice({
1183
1245
  });
1184
1246
  },
1185
1247
  });
1186
- export const { acceptMasterTOS, acceptMasterTOSFailure, acceptMasterTOSSuccess, applyAiCfoSidePanelHostPageTransition, clearAiCfoSidePanelHostPageContext, clearCurrentSessionId, clearDeleteChatSessionStatus, clearLastContextMessage, clearSkillDetail, clearSkillMutation, closeRoutineRuns, closeRoutinesBrowser, closeSkillsBrowser, createSession, createSessionAndSubmit, createSessionAndSubmitFailure, createSessionAndSubmitSuccess, createSessionFailure, createSessionSuccess, deleteChatSession, deleteChatSessionFailure, deleteChatSessionSuccess, deleteRoutine, deleteRoutineFailure, deleteRoutineSuccess, deleteSkill, deleteSkillFailure, deleteSkillSuccess, fetchChatHistory, fetchChatHistoryFailure, fetchChatHistorySuccess, fetchChatSessionsForUser, fetchChatSessionsForUserFailure, fetchChatSessionsForUserSuccess, fetchRoutineRuns, fetchRoutineRunsFailure, fetchRoutineRunsSuccess, fetchRoutines, fetchRoutinesFailure, fetchRoutinesSuccess, fetchSkillDetail, fetchSkillDetailFailure, fetchSkillDetailSuccess, fetchSkills, fetchSkillsFailure, fetchSkillsSuccess, fetchSkillVersionBody, fetchSkillVersionBodyFailure, fetchSkillVersionBodySuccess, fetchSkillVersions, fetchSkillVersionsFailure, fetchSkillVersionsSuccess, fetchSuggestedQuestions, fetchSuggestedQuestionsFailure, fetchSuggestedQuestionsSuccess, initializeNewSessionState, openRoutinesBrowser, openSkillsBrowser, runRoutine, runRoutineFailure, runRoutineSuccess, saveRoutine, saveRoutineFailure, saveRoutineSuccess, saveSkill, saveSkillFailure, saveSkillSuccess, setRoutineEnabled, setSession, setSkillVisibility, setSkillVisibilityFailure, setSkillVisibilitySuccess, stopSubmit, stopSubmitFailure, stopSubmitQuestion, stopSubmitQuestionFailure, stopSubmitQuestionSuccess, stopSubmitSuccess, submitFeedback, submitFeedbackFailure, submitFeedbackSuccess, submitQuestion, submitQuestionFailure, submitQuestionSuccess, updateAiCfoViewScrollPosition, updateCotCollapsedState, updateCurrentInput, updateResponseState, } = aiCfoView.actions;
1248
+ export const { acceptMasterTOS, acceptMasterTOSFailure, acceptMasterTOSSuccess, applyAiCfoSidePanelHostPageTransition, clearAiCfoSidePanelHostPageContext, clearCurrentSessionId, clearDeleteChatSessionStatus, clearLastContextMessage, clearSkillDetail, clearSkillMutation, closeRoutineRuns, closeRoutinesBrowser, closeSkillsBrowser, createSession, createSessionAndSubmit, createSessionAndSubmitFailure, createSessionAndSubmitSuccess, createSessionFailure, createSessionSuccess, deleteChatSession, deleteChatSessionFailure, deleteChatSessionSuccess, deleteRoutine, deleteRoutineFailure, deleteRoutineSuccess, deleteSkill, deleteSkillFailure, deleteSkillSuccess, fetchChatHistory, fetchChatHistoryFailure, fetchChatHistorySuccess, fetchChatSessionsForUser, fetchChatSessionsForUserFailure, fetchChatSessionsForUserSuccess, fetchRoutineRuns, fetchRoutineRunsFailure, fetchRoutineRunsSuccess, fetchRoutines, fetchRoutinesFailure, fetchRoutinesSuccess, fetchSkillDetail, fetchSkillDetailFailure, fetchSkillDetailSuccess, fetchSkills, fetchSkillsFailure, fetchSkillsSuccess, fetchSkillVersionBody, fetchSkillVersionBodyFailure, fetchSkillVersionBodySuccess, fetchSkillVersions, fetchSkillVersionsFailure, fetchSkillVersionsSuccess, fetchSuggestedQuestions, fetchSuggestedQuestionsFailure, fetchSuggestedQuestionsSuccess, initializeNewSessionState, openRoutinesBrowser, openSkillsBrowser, runRoutine, runRoutineFailure, runRoutineSuccess, saveRoutine, saveRoutineFailure, saveRoutineSuccess, saveSkill, saveSkillFailure, saveSkillSuccess, setRoutineEnabled, setSession, setSkillVisibility, setSkillVisibilityFailure, setSkillVisibilitySuccess, stopSubmit, stopSubmitFailure, stopSubmitQuestion, stopSubmitQuestionFailure, stopSubmitQuestionSuccess, stopSubmitSuccess, submitFeedback, submitFeedbackFailure, submitFeedbackSuccess, submitQuestion, submitQuestionFailure, submitQuestionSuccess, transcribeAudio, transcribeAudioFailure, transcribeAudioSuccess, updateAiCfoViewScrollPosition, updateCotCollapsedState, updateCurrentInput, updateResponseState, } = aiCfoView.actions;
1187
1249
  export default aiCfoView.reducer;
@@ -133,3 +133,28 @@ export function getSkillMutation(state) {
133
133
  export function getSkillsEnabled(state) {
134
134
  return getSkills(state).skillsEnabled;
135
135
  }
136
+ /** True while a finished voice recording is being transcribed server-side. */
137
+ /**
138
+ * A take belongs to the chat it was dictated into. A new chat has no session id yet on
139
+ * either side, which is a match rather than a mismatch.
140
+ */
141
+ function isTranscribingCurrentSession(state) {
142
+ if (state.aiCfoViewState == null) {
143
+ return false;
144
+ }
145
+ return (state.aiCfoViewState.transcribingChatSessionId ===
146
+ state.aiCfoViewState.uiState.currentChatSessionId);
147
+ }
148
+ export function getIsTranscribing(state) {
149
+ return (state.aiCfoViewState?.transcriptionState?.fetchState === 'In-Progress' &&
150
+ isTranscribingCurrentSession(state));
151
+ }
152
+ /**
153
+ * Set when a transcription request failed. Without surfacing this the composer
154
+ * would just drop the "Transcribing…" label and sit there silently, leaving the
155
+ * user with no sign their recording went nowhere.
156
+ */
157
+ export function getTranscriptionFailed(state) {
158
+ return (state.aiCfoViewState?.transcriptionState?.fetchState === 'Error' &&
159
+ isTranscribingCurrentSession(state));
160
+ }
@@ -0,0 +1,60 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, finalize, map, switchMap, takeUntil, timeout, } from 'rxjs/operators';
3
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../responsePayload';
4
+ import { clearAll } from '../../../rootActions';
5
+ import { transcribeAudio, transcribeAudioFailure, transcribeAudioSuccess, } from '../aiCfoViewReducer';
6
+ const AUDIO_FIELD = 'audio';
7
+ const LANGUAGE_FIELD = 'language';
8
+ /** Extension only has to match the recorded format; the server reads the part's MIME type. */
9
+ const AUDIO_FILE_NAME = 'dictation.wav';
10
+ /**
11
+ * A hung request would otherwise pin `isTranscribing` true forever, and the composer
12
+ * gates both submit and the mic on it — leaving the user unable to send even typed
13
+ * text until something else resolved it.
14
+ */
15
+ const TRANSCRIPTION_TIMEOUT_MS = 30000;
16
+ /**
17
+ * Voice dictation for the AI CFO composer. Dispatch `transcribeAudio(blob, language)` with a
18
+ * finished recording to POST it to the AI CFO micro-service `/1.0/transcriptions` endpoint;
19
+ * on success the transcript is appended to the composer input for the user to edit and send.
20
+ *
21
+ * `switchMap` (not `mergeMap`): a newer recording supersedes an older in-flight one, so a
22
+ * slow response can never append a stale transcript after a fresh one has already landed.
23
+ * The AbortController makes that cancellation real — `fetch` wrapped in a promise ignores
24
+ * an RxJS unsubscribe, so without the signal the superseded request would run to completion
25
+ * and still bill a transcription call whose result nothing wants.
26
+ */
27
+ export const transcribeAudioEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(transcribeAudio.match), switchMap((action) => {
28
+ const { audio, chatSessionId, language } = action.payload;
29
+ const abortController = new AbortController();
30
+ const formData = new FormData();
31
+ formData.append(AUDIO_FIELD, audio, AUDIO_FILE_NAME);
32
+ if (language != null && language !== '') {
33
+ formData.append(LANGUAGE_FIELD, language);
34
+ }
35
+ return zeniAPI
36
+ .postFormData(`${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/transcriptions`, formData, abortController.signal)
37
+ .pipe(timeout(TRANSCRIPTION_TIMEOUT_MS),
38
+ // A tenant switch clears the store; a transcript still in flight must not
39
+ // land in the next tenant's composer (both sessions can be `undefined`, so
40
+ // the reducer's session guard would let it through).
41
+ takeUntil(actions$.pipe(filter(clearAll.match))), map((response) => {
42
+ if (!isSuccessResponse(response)) {
43
+ return transcribeAudioFailure(response.status);
44
+ }
45
+ // An EMPTY transcript is a success (the mic caught silence) and the
46
+ // reducer leaves the composer alone. A MISSING one breaks the response
47
+ // contract, so it surfaces as an error rather than being coerced to
48
+ // silence the user cannot distinguish from a quiet room.
49
+ if (response.data?.transcript == null) {
50
+ return transcribeAudioFailure(createZeniAPIStatus('Transcription succeeded but the response carried no transcript', 'Unexpected Error'));
51
+ }
52
+ // The session id rides along so a transcript that lands after the user
53
+ // switched sessions is dropped rather than misfiled.
54
+ return transcribeAudioSuccess(response.data.transcript, chatSessionId);
55
+ }), finalize(() => abortController.abort()), catchError((error) => of(transcribeAudioFailure(createZeniAPIStatus(
56
+ // message (detailed) first, then the short title — per createZeniAPIStatus(message, title).
57
+ // JSON.stringify(Error) drops the (non-enumerable) message — extract it explicitly.
58
+ 'Transcription REST API call errored out: ' +
59
+ (error instanceof Error ? error.message : String(error)), 'Unexpected Error')))));
60
+ }));
@@ -4,15 +4,20 @@ import { stringToUnion } from '../../../commonStateTypes/stringToUnion';
4
4
  * verbatim to the listing API as `transaction_group`. When revenue automation is
5
5
  * enabled for the tenant the backend scopes the whole view by the line
6
6
  * account_type: `'expense'` -> expense accounts, `'income'` -> income accounts,
7
- * `'all'` -> both. When off, every value falls back to the legacy expense scope
8
- * on the backend, so the group is only sent when the gate is on.
7
+ * `'other'` -> balance-sheet accounts other than bank/credit card (plus lines
8
+ * with no account type), `'all'` -> all three. The list epics always send it;
9
+ * when the gate is off the backend ignores it and returns the legacy expense
10
+ * scope.
9
11
  *
10
12
  * The wire value for expenses is the singular `'expense'` (matches the backend
11
13
  * `TransactionGroup` enum) even though the picker labels it "Expenses".
12
- * Balance-sheet groups (assets, liability) are reserved for later and are not
13
- * part of this union yet.
14
14
  */
15
- export const TRANSACTION_GROUPS = ['all', 'income', 'expense'];
15
+ export const TRANSACTION_GROUPS = [
16
+ 'all',
17
+ 'income',
18
+ 'expense',
19
+ 'other',
20
+ ];
16
21
  export const toTransactionGroup = (v) => stringToUnion(v != null ? v.trim().toLowerCase() : '', TRANSACTION_GROUPS);
17
22
  /**
18
23
  * Default group for the listing — matches the picker's default selection ("All").
@@ -26,7 +26,6 @@ export const getTaskDetail = (state, taskId) => {
26
26
  let isDeleted = undefined;
27
27
  let depth = undefined;
28
28
  let recurringSourceTaskId = undefined;
29
- let taskCategoryId = undefined;
30
29
  let snoozedUntil = undefined;
31
30
  let taskGroupId = undefined;
32
31
  let subtasks = [];
@@ -40,7 +39,6 @@ export const getTaskDetail = (state, taskId) => {
40
39
  if (taskEntity != null) {
41
40
  createdByUser = getUserAndUserRole(userState, userRoleState, addressState, taskEntity.createdBy);
42
41
  depth = taskEntity.depth;
43
- taskCategoryId = taskEntity.taskCategoryId;
44
42
  isArchived = taskEntity.isArchived;
45
43
  isDeleted = taskEntity.isDeleted;
46
44
  recurringSourceTaskId = taskEntity.recurringSourceTaskId;
@@ -105,7 +103,6 @@ export const getTaskDetail = (state, taskId) => {
105
103
  version: 0,
106
104
  createdByUser,
107
105
  depth,
108
- taskCategoryId,
109
106
  isArchived,
110
107
  isDeleted,
111
108
  recurringSourceTaskId,
@@ -47,13 +47,7 @@ export const TASK_LIST_FILTER_CATEGORIES = [
47
47
  type: 'user',
48
48
  multiple: true,
49
49
  },
50
- {
51
- value: 'createdBy',
52
- type: 'user',
53
- multiple: true,
54
- },
55
50
  { value: 'creationDate', type: 'dateRange', multiple: true },
56
- { value: 'billing', type: 'dropdown', multiple: true },
57
51
  ];
58
52
  export const TASK_LIST_GROUP_BY_CATEGORIES = [
59
53
  { value: 'status' },
@@ -1,8 +1,3 @@
1
- /** Billing lives on the task's category, not the task, so it needs the catalog. */
2
- export const buildCategoryValueGetter = (taskCategoriesById) => (key, task) => key === 'billing'
3
- ? (taskCategoriesById[task.taskCategoryId ?? '']?.billingType ??
4
- 'non_billable')
5
- : getCategoryValueForTaskList(key, task);
6
1
  export const getCategoryValueForTaskList = (key, task) => {
7
2
  switch (key) {
8
3
  case 'dueDate':
@@ -17,8 +12,6 @@ export const getCategoryValueForTaskList = (key, task) => {
17
12
  return task.createTime;
18
13
  case 'assignee':
19
14
  return task.assignees;
20
- case 'createdBy':
21
- return task.createdBy;
22
15
  default:
23
16
  return undefined;
24
17
  }
@@ -15,7 +15,7 @@ import { getUserGroupList } from '../../userGroupListView/userGroupListViewSelec
15
15
  import { getUserList, } from '../../userListView/userListViewSelector';
16
16
  import { allTaskPriority, allTaskStatus, } from '../taskDetailView/taskDetailSelector';
17
17
  import { DUE_DATE_GROUP_KEYS, } from './taskList';
18
- import { buildCategoryValueGetter } from './taskListFilterHelpers';
18
+ import { getCategoryValueForTaskList } from './taskListFilterHelpers';
19
19
  const sortTasksList = (tagState, tasksList, sortKey, sortOrder) => {
20
20
  const tasksInOrder = orderBy(tasksList, (task) => {
21
21
  switch (sortKey) {
@@ -47,7 +47,7 @@ const sortTasksList = (tagState, tasksList, sortKey, sortOrder) => {
47
47
  }, sortOrder);
48
48
  return tasksInOrder;
49
49
  };
50
- const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRoleState, addressState, taskIds, tagState, userGroupsState, taskCategoriesById, }) => {
50
+ const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRoleState, addressState, taskIds, tagState, userGroupsState, }) => {
51
51
  const { sortKey, searchText } = taskListState.uiState;
52
52
  const sortOrder = getSortOrder(taskListState.uiState.sortOrder);
53
53
  const tasks = taskIds
@@ -70,11 +70,11 @@ const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRole
70
70
  return undefined;
71
71
  })
72
72
  .filter((value) => value?.name.toLowerCase().includes(searchText.toLowerCase()) === true);
73
- const filteredTaskList = applyAdvancedFiltersOnList(buildCategoryValueGetter(taskCategoriesById), tasks, taskListState.filters);
73
+ const filteredTaskList = applyAdvancedFiltersOnList(getCategoryValueForTaskList, tasks, taskListState.filters);
74
74
  const sortedTaskList = sortTasksList(tagState, filteredTaskList, sortKey, sortOrder);
75
75
  return sortedTaskList;
76
76
  };
77
- const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupState, taskGroupsIds, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
77
+ const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupState, taskGroupsIds, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
78
78
  const taskGroupsWithTasksList = taskGroupsIds
79
79
  .map((taskGroupId) => {
80
80
  const taskGroup = getTaskGroupById(taskGroupState, taskGroupId);
@@ -88,7 +88,6 @@ const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupStat
88
88
  taskIds,
89
89
  tagState,
90
90
  userGroupsState,
91
- taskCategoriesById,
92
91
  });
93
92
  if (taskGroup != null) {
94
93
  return {
@@ -120,7 +119,7 @@ const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupStat
120
119
  // Also hides deleted groups, which are empty as a side effect.
121
120
  return sortedTaskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
122
121
  };
123
- export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
122
+ export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
124
123
  const assigneeGroups = Object.keys(taskListState.taskIdsByAssignees);
125
124
  const taskGroupsWithTasksList = assigneeGroups.map((groupKey) => {
126
125
  const taskIds = taskListState.taskIdsByAssignees[groupKey];
@@ -133,7 +132,6 @@ export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userSt
133
132
  taskIds,
134
133
  tagState,
135
134
  userGroupsState,
136
- taskCategoriesById,
137
135
  });
138
136
  const assigneeIds = groupKey.split(',');
139
137
  const groupName = getUsersByUserIds(userState, assigneeIds)
@@ -151,7 +149,7 @@ export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userSt
151
149
  });
152
150
  return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
153
151
  };
154
- export const getTaskListGroupedByPriority = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
152
+ export const getTaskListGroupedByPriority = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
155
153
  const taskGroupsWithTasksList = ALL_PRIORITY_CODE.map((groupKey) => {
156
154
  const taskIds = taskListState.taskIdsByPriority[groupKey] ?? [];
157
155
  const tasksWithUserDetails = getTasksWithUserDetails({
@@ -163,7 +161,6 @@ export const getTaskListGroupedByPriority = ({ taskListState, taskState, userSta
163
161
  taskIds,
164
162
  tagState,
165
163
  userGroupsState,
166
- taskCategoriesById,
167
164
  });
168
165
  return {
169
166
  groupName: allTaskPriority.find((priority) => priority.code === groupKey)?.name ??
@@ -175,7 +172,7 @@ export const getTaskListGroupedByPriority = ({ taskListState, taskState, userSta
175
172
  });
176
173
  return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
177
174
  };
178
- const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
175
+ const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
179
176
  const taskGroupsWithTasksList = ALL_TASK_STATUS_CODE.map((groupKey) => {
180
177
  const taskIds = taskListState.taskIdsByStatus[groupKey] ?? [];
181
178
  const tasksWithUserDetails = getTasksWithUserDetails({
@@ -187,7 +184,6 @@ const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userR
187
184
  taskIds,
188
185
  tagState,
189
186
  userGroupsState,
190
- taskCategoriesById,
191
187
  });
192
188
  return {
193
189
  groupName: allTaskStatus.find((taskStatus) => taskStatus.code === groupKey)
@@ -199,7 +195,7 @@ const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userR
199
195
  });
200
196
  return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
201
197
  };
202
- const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
198
+ const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
203
199
  const tagGroups = Object.keys(taskListState.taskIdsByTags);
204
200
  const taskGroupsWithTasksList = tagGroups.map((groupKey) => {
205
201
  const taskIds = taskListState.taskIdsByTags[groupKey];
@@ -212,7 +208,6 @@ const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRol
212
208
  taskIds,
213
209
  tagState,
214
210
  userGroupsState,
215
- taskCategoriesById,
216
211
  });
217
212
  const tagIds = groupKey.split(',');
218
213
  const groupName = getTagsByIds(tagState, tagIds)
@@ -229,7 +224,7 @@ const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRol
229
224
  });
230
225
  return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
231
226
  };
232
- const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
227
+ const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
233
228
  const taskGroupsWithTasksList = DUE_DATE_GROUP_KEYS.map((groupKey) => {
234
229
  const taskIds = taskListState.taskIdsByDueDate[groupKey];
235
230
  const tasksWithUserDetails = getTasksWithUserDetails({
@@ -241,7 +236,6 @@ const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, user
241
236
  taskIds,
242
237
  tagState,
243
238
  userGroupsState,
244
- taskCategoriesById,
245
239
  });
246
240
  return {
247
241
  groupName: groupKey,
@@ -252,7 +246,7 @@ const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, user
252
246
  });
253
247
  return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
254
248
  };
255
- const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState, taskCategoriesById, }) => {
249
+ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState, }) => {
256
250
  const { groupByKey } = taskListState.uiState;
257
251
  if (groupByKey === 'groupName') {
258
252
  return getTaskListGroupedByGroupName({
@@ -265,7 +259,6 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
265
259
  taskGroupsIds,
266
260
  tagState,
267
261
  userGroupsState,
268
- taskCategoriesById,
269
262
  });
270
263
  }
271
264
  if (groupByKey === 'assignee') {
@@ -277,7 +270,6 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
277
270
  addressState,
278
271
  tagState,
279
272
  userGroupsState,
280
- taskCategoriesById,
281
273
  });
282
274
  }
283
275
  if (groupByKey === 'priority') {
@@ -289,7 +281,6 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
289
281
  addressState,
290
282
  tagState,
291
283
  userGroupsState,
292
- taskCategoriesById,
293
284
  });
294
285
  }
295
286
  if (groupByKey === 'status') {
@@ -301,7 +292,6 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
301
292
  addressState,
302
293
  tagState,
303
294
  userGroupsState,
304
- taskCategoriesById,
305
295
  });
306
296
  }
307
297
  if (groupByKey === 'tag') {
@@ -313,7 +303,6 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
313
303
  addressState,
314
304
  tagState,
315
305
  userGroupsState,
316
- taskCategoriesById,
317
306
  });
318
307
  }
319
308
  return getTaskListGroupedByDueDate({
@@ -324,7 +313,6 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
324
313
  addressState,
325
314
  tagState,
326
315
  userGroupsState,
327
- taskCategoriesById,
328
316
  });
329
317
  };
330
318
  /**
@@ -349,10 +337,7 @@ const getTaskGroupsWithTaskListMemoized = createSelector((state) => state.taskLi
349
337
  // invalidate this memo and rebuild a board the user is already using.
350
338
  (state) => tagsAreStructural(state.taskListState.uiState)
351
339
  ? state.tagState
352
- : EMPTY_TAG_STATE, (state) => state.userGroupsState,
353
- // Only the catalog, not the whole timeEntry slice: billing is read off
354
- // the task's category, and a timer tick must not rebuild the board.
355
- (state) => state.timeEntryState.taskCategoriesById, (uiState, filters, taskIdsByGroupsIds, taskIdsByAssignees, taskIdsByPriority, taskIdsByStatus, taskIdsByTags, taskIdsByDueDate, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState, taskCategoriesById) => getTaskGroupsWithTaskList({
340
+ : EMPTY_TAG_STATE, (state) => state.userGroupsState, (uiState, filters, taskIdsByGroupsIds, taskIdsByAssignees, taskIdsByPriority, taskIdsByStatus, taskIdsByTags, taskIdsByDueDate, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState) => getTaskGroupsWithTaskList({
356
341
  taskListState: {
357
342
  uiState,
358
343
  filters,
@@ -371,7 +356,6 @@ const getTaskGroupsWithTaskListMemoized = createSelector((state) => state.taskLi
371
356
  taskGroupsIds,
372
357
  tagState,
373
358
  userGroupsState,
374
- taskCategoriesById,
375
359
  }));
376
360
  export const getAllTasks = createSelector(
377
361
  // `taskState`, `taskGroupState`, `addressState` and `tagState` are gone from
@@ -1,4 +1,3 @@
1
- import { convertHHMMStrToMinutes } from '../../../commonStateTypes/fiscalYearHelpers/formatMinutesToFromHHMM';
2
1
  import { dateNow } from '../../../zeniDayJS';
3
2
  import { allTaskPriority, allTaskStatus, } from '../taskDetailView/taskDetailSelector';
4
3
  import { toDueDateGroupKeyType } from './taskList';
@@ -64,11 +63,6 @@ export const sortSubtasks = (subtasks, sortKey, sortOrder, userList, tagList) =>
64
63
  return acc;
65
64
  }, {});
66
65
  const unknownStatusRank = allTaskStatus.length;
67
- const priorityOrder = allTaskPriority.reduce((acc, priority, index) => {
68
- acc[priority.code] = index;
69
- return acc;
70
- }, {});
71
- const unknownPriorityRank = allTaskPriority.length;
72
66
  const cmpStr = (a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' });
73
67
  const keyFn = (task) => {
74
68
  if (sortKey === 'name') {
@@ -95,37 +89,6 @@ export const sortSubtasks = (subtasks, sortKey, sortOrder, userList, tagList) =>
95
89
  const tag = tagList.find((candidate) => candidate.tagId === firstTagId);
96
90
  return tag?.name ?? '';
97
91
  }
98
- if (sortKey === 'priority') {
99
- // Rank by the canonical order, not the code string: alphabetical would
100
- // sort "high" before "urgent" and read as wrong to anyone scanning it.
101
- return priorityOrder[task.priority?.code] ?? unknownPriorityRank;
102
- }
103
- if (sortKey === 'dueDate') {
104
- // A task with no date is not "earliest", so undated rows belong at the
105
- // bottom in BOTH directions. The comparator negates for 'desc', so one
106
- // fixed sentinel would flip them to the top — the sentinel has to flip
107
- // with the order to survive that negation.
108
- // `dueDate` is already a ZeniDate (dayjs), so valueOf() is the epoch.
109
- // An unparseable date yields NaN, and a NaN comparator leaves the array
110
- // untouched -- the same silent failure the timeSpent branch guards.
111
- // Treat it as undated rather than as a date.
112
- const due = task.dueDate;
113
- const epoch = due?.valueOf();
114
- if (epoch == null || Number.isNaN(epoch)) {
115
- return sortOrder === 'asc'
116
- ? Number.MAX_SAFE_INTEGER
117
- : Number.MIN_SAFE_INTEGER;
118
- }
119
- return epoch;
120
- }
121
- if (sortKey === 'timeSpent') {
122
- // `timeSpent` is an "HHMM" STRING (no colon). Comparing it as text puts
123
- // "1000" before "0900" only by luck of padding, and any malformed value
124
- // makes the converter return NaN -- which a comparator turns into "no
125
- // sort at all", silently. Coerce that to 0.
126
- const minutes = convertHHMMStrToMinutes(task.timeSpent ?? '');
127
- return Number.isFinite(minutes) ? minutes : 0;
128
- }
129
92
  return '';
130
93
  };
131
94
  const copy = [...subtasks];
@@ -131,13 +131,29 @@ export const prepareCreateTransactionPayload = (state, localData) => {
131
131
  };
132
132
  }
133
133
  const lines = localData.lines.map((line) => toLinePayload(state, line, transactionType));
134
+ const documentIdField = localData.documentNumber != null && localData.documentNumber !== ''
135
+ ? { document_id: localData.documentNumber }
136
+ : {};
134
137
  if (transactionType === 'journal_entry') {
138
+ return { ...base, ...documentIdField, lines };
139
+ }
140
+ // Both post to accounts payable, so they carry no header account or payment type.
141
+ if (transactionType === 'vendor_credit') {
142
+ return {
143
+ ...base,
144
+ vendor_id: toQboVendorId(state, localData.vendorId),
145
+ ...documentIdField,
146
+ lines,
147
+ };
148
+ }
149
+ if (transactionType === 'bill') {
135
150
  return {
136
151
  ...base,
137
- ...(localData.documentNumber != null &&
138
- localData.documentNumber !== '' && {
139
- document_id: localData.documentNumber,
152
+ vendor_id: toQboVendorId(state, localData.vendorId),
153
+ ...(localData.dueDate != null && {
154
+ due_date: localData.dueDate.format(DEFAULT_DATE_FORMAT),
140
155
  }),
156
+ ...documentIdField,
141
157
  lines,
142
158
  };
143
159
  }
@@ -8,4 +8,6 @@ export const ALL_CREATABLE_TRANSACTION_TYPES = [
8
8
  'credit_card_credit',
9
9
  'transfer',
10
10
  'journal_entry',
11
+ 'bill',
12
+ 'vendor_credit',
11
13
  ];