From 627f2e0b45f6b93fdd944c97ce51bc471132e8a3 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 14 Oct 2026 09:16:01 -0700 Subject: [PATCH 1/2] RR-T51 Department Changeable Names, Base Status Types fixes --- src/api/config/terminology.ts | 11 + .../(app)/__tests__/init-hub-connect.test.tsx | 18 ++ src/app/(app)/_layout.tsx | 11 +- src/app/(app)/calendar.tsx | 4 +- src/app/(app)/checklists.tsx | 4 +- src/app/(app)/home/__tests__/_layout.test.tsx | 63 ++++++ src/app/(app)/home/_layout.tsx | 10 +- src/app/(app)/map.tsx | 6 +- src/app/(app)/records.tsx | 4 +- src/app/(app)/shifts.tsx | 4 +- src/app/(app)/work-orders/[id].tsx | 6 +- src/app/(app)/work-orders/index.tsx | 6 +- src/app/call/[id]/edit.tsx | 9 +- src/app/records/[id].tsx | 14 +- src/app/records/__tests__/[id].test.tsx | 14 ++ src/app/records/new.tsx | 6 +- .../calendar/enhanced-calendar-view.tsx | 4 +- .../home/__tests__/status-buttons.test.tsx | 32 +++ src/components/home/status-buttons.tsx | 11 +- .../__tests__/record-list-item.test.tsx | 158 +++++++++++++ src/components/records/record-list-item.tsx | 11 +- .../sidebar/__tests__/side-menu.test.tsx | 40 +++- src/components/sidebar/side-menu-content.tsx | 21 +- .../units/__tests__/unit-card.test.tsx | 111 ++++++++++ src/components/units/unit-card.tsx | 15 +- src/hooks/use-record-definition-name.ts | 52 +++++ src/lib/__tests__/base-types.test.ts | 84 +++++++ src/lib/__tests__/call-field-policy.test.ts | 27 +++ src/lib/base-types.ts | 207 ++++++++++++++++++ src/lib/call-field-policy.ts | 18 ++ .../callPriorities/callPriorityResultData.ts | 2 + src/models/v4/callTypes/callTypeResultData.ts | 2 + src/models/v4/calls/callResultData.ts | 2 + src/models/v4/configs/getTerminologyResult.ts | 17 ++ .../device/nativePushUnRegistrationInput.ts | 5 + .../v4/personnel/personnelInfoResultData.ts | 6 + .../v4/security/departmentRightsResultData.ts | 8 + src/models/v4/statuses/statusesResultData.ts | 4 +- src/models/v4/units/unitInfoResultData.ts | 2 + .../push-notification-sign-out.test.ts | 137 ++++++++++++ src/services/push-notification.ts | 40 +++- src/stores/home/__tests__/home-store.test.ts | 80 +++++++ src/stores/home/home-store.ts | 6 +- .../terminology/__tests__/store.test.ts | 116 ++++++++++ src/stores/terminology/store.ts | 140 ++++++++++++ 45 files changed, 1495 insertions(+), 53 deletions(-) create mode 100644 src/api/config/terminology.ts create mode 100644 src/app/(app)/home/__tests__/_layout.test.tsx create mode 100644 src/components/records/__tests__/record-list-item.test.tsx create mode 100644 src/components/units/__tests__/unit-card.test.tsx create mode 100644 src/hooks/use-record-definition-name.ts create mode 100644 src/lib/__tests__/base-types.test.ts create mode 100644 src/lib/base-types.ts create mode 100644 src/models/v4/configs/getTerminologyResult.ts create mode 100644 src/models/v4/device/nativePushUnRegistrationInput.ts create mode 100644 src/services/__tests__/push-notification-sign-out.test.ts create mode 100644 src/stores/home/__tests__/home-store.test.ts create mode 100644 src/stores/terminology/__tests__/store.test.ts create mode 100644 src/stores/terminology/store.ts diff --git a/src/api/config/terminology.ts b/src/api/config/terminology.ts new file mode 100644 index 00000000..48e65948 --- /dev/null +++ b/src/api/config/terminology.ts @@ -0,0 +1,11 @@ +import { type GetTerminologyResult } from '@/models/v4/configs/getTerminologyResult'; + +import { createApiEndpoint } from '../common/client'; + +const getTerminologyApi = createApiEndpoint('/Config/GetTerminology'); + +/** The department's own names for modules and built-in record types (setting 118). */ +export const getTerminology = async () => { + const response = await getTerminologyApi.get(); + return response.data; +}; diff --git a/src/app/(app)/__tests__/init-hub-connect.test.tsx b/src/app/(app)/__tests__/init-hub-connect.test.tsx index d7c765bb..09c0e0f0 100644 --- a/src/app/(app)/__tests__/init-hub-connect.test.tsx +++ b/src/app/(app)/__tests__/init-hub-connect.test.tsx @@ -123,6 +123,11 @@ jest.mock('@/stores/feature-flags/store', () => ({ featureFlagsStore: { getState: () => ({ fetchFlags: jest.fn().mockResolvedValue(undefined), isEnabled: mockIsChatEnabled }) }, })); +const mockFetchTerminology = jest.fn().mockResolvedValue(undefined); +jest.mock('@/stores/terminology/store', () => ({ + terminologyStore: { getState: () => ({ fetchTerminology: mockFetchTerminology }) }, +})); + const mockConnectUpdateHub = jest.fn().mockResolvedValue(undefined); const mockConnectGeolocationHub = jest.fn().mockResolvedValue(undefined); const mockConnectChatHub = jest.fn().mockResolvedValue(undefined); @@ -148,6 +153,7 @@ describe('app initialization connects the SignalR update hub', () => { mockConnectUpdateHub.mockResolvedValue(undefined); mockConnectGeolocationHub.mockResolvedValue(undefined); mockConnectChatHub.mockResolvedValue(undefined); + mockFetchTerminology.mockResolvedValue(undefined); mockUseSignalRLifecycle.mockImplementation(({ hasInitialized }) => { lifecycleCalls.push({ hasInitialized, updateConnectsSoFar: mockConnectUpdateHub.mock.calls.length }); return undefined as unknown as ReturnType; @@ -162,6 +168,17 @@ describe('app initialization connects the SignalR update hub', () => { }); }); + it('fetches the department terminology after rights and before the hubs connect', async () => { + render(); + + await waitFor(() => { + expect(mockConnectUpdateHub).toHaveBeenCalledTimes(1); + }); + expect(mockFetchTerminology).toHaveBeenCalledTimes(1); + expect(mockSecurityState.getRights.mock.invocationCallOrder[0]).toBeLessThan(mockFetchTerminology.mock.invocationCallOrder[0]); + expect(mockFetchTerminology.mock.invocationCallOrder[0]).toBeLessThan(mockConnectUpdateHub.mock.invocationCallOrder[0]); + }); + it('connects the update hub before arming useSignalRLifecycle, so the two cannot both connect', async () => { render(); @@ -221,5 +238,6 @@ describe('app initialization connects the SignalR update hub', () => { expect(mockConnectUpdateHub).not.toHaveBeenCalled(); expect(mockConnectGeolocationHub).not.toHaveBeenCalled(); expect(mockConnectChatHub).not.toHaveBeenCalled(); + expect(mockFetchTerminology).not.toHaveBeenCalled(); }); }); diff --git a/src/app/(app)/_layout.tsx b/src/app/(app)/_layout.tsx index a7c72ceb..f9e6f5c0 100644 --- a/src/app/(app)/_layout.tsx +++ b/src/app/(app)/_layout.tsx @@ -47,6 +47,7 @@ import { usePersonnelStore } from '@/stores/personnel/store'; import { useRolesStore } from '@/stores/roles/store'; import { securityStore } from '@/stores/security/store'; import { useSignalRStore } from '@/stores/signalr/signalr-store'; +import { terminologyStore } from '@/stores/terminology/store'; import { useWeatherAlertsStore } from '@/stores/weather-alerts/weather-alerts-store'; // Sets the SDK token now (stored server token, else the built-in one) and again whenever it changes. @@ -141,9 +142,10 @@ export default function TabLayout() { //await usePersonnelStore.getState().init(); if (!isCurrentRun()) return; - // Feature flags must follow rights: the identity key that decides whether persisted - // flags belong to this account reads securityStore.rights.DepartmentId. - await Promise.all([featureFlagsStore.getState().fetchFlags(), dataProtectionStore.getState().fetchCapabilities()]); + // Feature flags and terminology must follow rights: the identity key that decides whether + // persisted flags or names belong to this account reads securityStore.rights.DepartmentId. + // Terminology only relabels screens, and its fetch never rejects. + await Promise.all([featureFlagsStore.getState().fetchFlags(), dataProtectionStore.getState().fetchCapabilities(), terminologyStore.getState().fetchTerminology()]); if (!isCurrentRun()) return; // Realtime feeds. Every one of them has to follow the Promise.all above: opening a hub reads @@ -473,6 +475,9 @@ export default function TabLayout() { return ( ('today'); @@ -252,7 +254,7 @@ export default function CalendarScreen() { <> diff --git a/src/app/(app)/checklists.tsx b/src/app/(app)/checklists.tsx index c144d48b..4e7f0304 100644 --- a/src/app/(app)/checklists.tsx +++ b/src/app/(app)/checklists.tsx @@ -11,9 +11,11 @@ import { useChecklistLiveUpdates } from '@/hooks/use-checklist-live-updates'; import { useProtectedReveal } from '@/hooks/use-protected-reveal'; import { checklistError } from '@/lib/checklists/sync'; import { useChecklistsStore } from '@/stores/checklists/store'; +import { TermKeys, useTerm } from '@/stores/terminology/store'; export default function ChecklistsScreen() { const { t } = useTranslation(); + const checklistsTitle = useTerm(TermKeys.Checklists, t('checklists.labels.Checklists')); const { occurrenceId } = useLocalSearchParams<{ occurrenceId?: string }>(); const unitId: string | undefined = undefined; const state = useChecklistsStore(); @@ -46,7 +48,7 @@ export default function ChecklistsScreen() { const message = error ?? state.error; return ( - +