diff --git a/expo-env.d.ts b/expo-env.d.ts
index 5411fdde..bf3c1693 100644
--- a/expo-env.d.ts
+++ b/expo-env.d.ts
@@ -1,3 +1,3 @@
///
-// NOTE: This file should not be edited and should be in your git ignore
\ No newline at end of file
+// NOTE: This file should not be edited and should be in your git ignore
diff --git a/src/api/common/client.tsx b/src/api/common/client.tsx
index 2eda70c1..cd53425d 100644
--- a/src/api/common/client.tsx
+++ b/src/api/common/client.tsx
@@ -98,12 +98,26 @@ axiosInstance.interceptors.response.use(
return axiosInstance(originalRequest);
} catch (refreshError) {
processQueue(refreshError as Error);
- // Handle refresh token failure
- useAuthStore.getState().logout();
- logger.error({
- message: 'Token refresh failed',
- context: { error: refreshError },
- });
+
+ // Check if it's a network error vs an invalid refresh token
+ const isNetworkError =
+ refreshError instanceof Error &&
+ (refreshError.message.includes('Network Error') || refreshError.message.includes('timeout') || refreshError.message.includes('ECONNREFUSED') || refreshError.message.includes('ETIMEDOUT'));
+
+ if (!isNetworkError) {
+ // Only logout for non-network errors (e.g., invalid refresh token, 400/401 from token endpoint)
+ logger.error({
+ message: 'Token refresh failed with non-recoverable error, logging out user',
+ context: { error: refreshError },
+ });
+ useAuthStore.getState().logout();
+ } else {
+ logger.warn({
+ message: 'Token refresh failed due to network error',
+ context: { error: refreshError },
+ });
+ }
+
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
diff --git a/src/app/(app)/__tests__/protocols.test.tsx b/src/app/(app)/__tests__/protocols.test.tsx
index a74b76f3..859c5713 100644
--- a/src/app/(app)/__tests__/protocols.test.tsx
+++ b/src/app/(app)/__tests__/protocols.test.tsx
@@ -32,7 +32,7 @@ jest.mock('@/components/protocols/protocol-card', () => ({
ProtocolCard: ({ protocol, onPress }: { protocol: any; onPress: (id: string) => void }) => {
const { Pressable, Text } = require('react-native');
return (
- onPress(protocol.Id)}>
+ onPress(protocol.ProtocolId)}>
{protocol.Name}
);
@@ -108,7 +108,7 @@ jest.mock('@/stores/protocols/store', () => ({
// Mock protocols test data
const mockProtocols: CallProtocolsResultData[] = [
{
- Id: '1',
+ ProtocolId: '1',
DepartmentId: 'dept1',
Name: 'Fire Emergency Response',
Code: 'FIRE001',
@@ -126,7 +126,7 @@ const mockProtocols: CallProtocolsResultData[] = [
Questions: [],
},
{
- Id: '2',
+ ProtocolId: '2',
DepartmentId: 'dept1',
Name: 'Medical Emergency',
Code: 'MED001',
@@ -144,7 +144,7 @@ const mockProtocols: CallProtocolsResultData[] = [
Questions: [],
},
{
- Id: '3',
+ ProtocolId: '3',
DepartmentId: 'dept1',
Name: 'Hazmat Response',
Code: 'HAZ001',
@@ -162,7 +162,7 @@ const mockProtocols: CallProtocolsResultData[] = [
Questions: [],
},
{
- Id: '', // Empty ID to test the keyExtractor fix
+ ProtocolId: '', // Empty ID to test the keyExtractor fix
DepartmentId: 'dept1',
Name: 'Protocol with Empty ID',
Code: 'EMPTY001',
diff --git a/src/app/(app)/protocols.tsx b/src/app/(app)/protocols.tsx
index f59e64bb..35919dd3 100644
--- a/src/app/(app)/protocols.tsx
+++ b/src/app/(app)/protocols.tsx
@@ -39,6 +39,13 @@ export default function Protocols() {
setRefreshing(false);
}, [fetchProtocols]);
+ const handleProtocolPress = React.useCallback(
+ (id: string) => {
+ selectProtocol(id);
+ },
+ [selectProtocol]
+ );
+
const filteredProtocols = React.useMemo(() => {
if (!searchQuery.trim()) return protocols;
@@ -69,11 +76,13 @@ export default function Protocols() {
item.Id || `protocol-${index}`}
- renderItem={({ item }) => }
+ keyExtractor={(item, index) => item.ProtocolId || `protocol-${index}`}
+ renderItem={({ item }) => }
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 100 }}
refreshControl={}
+ extraData={handleProtocolPress}
+ estimatedItemSize={120}
/>
) : (
diff --git a/src/app/(app)/settings.tsx b/src/app/(app)/settings.tsx
index c20791c7..bfc265c9 100644
--- a/src/app/(app)/settings.tsx
+++ b/src/app/(app)/settings.tsx
@@ -1,7 +1,7 @@
/* eslint-disable react/react-in-jsx-scope */
import { Env } from '@env';
import { useColorScheme } from 'nativewind';
-import React, { useEffect } from 'react';
+import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { BackgroundGeolocationItem } from '@/components/settings/background-geolocation-item';
@@ -15,16 +15,35 @@ import { ThemeItem } from '@/components/settings/theme-item';
import { ToggleItem } from '@/components/settings/toggle-item';
import { UnitSelectionBottomSheet } from '@/components/settings/unit-selection-bottom-sheet';
import { FocusAwareStatusBar, ScrollView } from '@/components/ui';
+import { AlertDialog, AlertDialogBackdrop, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader } from '@/components/ui/alert-dialog';
import { Box } from '@/components/ui/box';
+import { Button, ButtonText } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Heading } from '@/components/ui/heading';
+import { Text } from '@/components/ui/text';
import { VStack } from '@/components/ui/vstack';
import { useAnalytics } from '@/hooks/use-analytics';
import { useAuth, useAuthStore } from '@/lib';
import { logger } from '@/lib/logging';
-import { getBaseApiUrl } from '@/lib/storage/app';
+import { storage } from '@/lib/storage';
+import { getBaseApiUrl, removeActiveCallId, removeActiveUnitId, removeDeviceUuid } from '@/lib/storage/app';
import { openLinkInBrowser } from '@/lib/utils';
+import { useAudioStreamStore } from '@/stores/app/audio-stream-store';
+import { useBluetoothAudioStore } from '@/stores/app/bluetooth-audio-store';
import { useCoreStore } from '@/stores/app/core-store';
+import { useLiveKitStore } from '@/stores/app/livekit-store';
+import { useLoadingStore } from '@/stores/app/loading-store';
+import { useLocationStore } from '@/stores/app/location-store';
+import { useCallsStore } from '@/stores/calls/store';
+import { useContactsStore } from '@/stores/contacts/store';
+import { useDispatchStore } from '@/stores/dispatch/store';
+import { useNotesStore } from '@/stores/notes/store';
+import { useOfflineQueueStore } from '@/stores/offline-queue/store';
+import { useProtocolsStore } from '@/stores/protocols/store';
+import { usePushNotificationModalStore } from '@/stores/push-notification/store';
+import { useRolesStore } from '@/stores/roles/store';
+import { securityStore } from '@/stores/security/store';
+import { useStatusBottomSheetStore } from '@/stores/status/store';
import { useUnitsStore } from '@/stores/units/store';
export default function Settings() {
@@ -36,6 +55,7 @@ export default function Settings() {
const { login, status, isAuthenticated } = useAuth();
const [showServerUrl, setShowServerUrl] = React.useState(false);
const [showUnitSelection, setShowUnitSelection] = React.useState(false);
+ const [showLogoutConfirm, setShowLogoutConfirm] = React.useState(false);
const activeUnit = useCoreStore((state) => state.activeUnit);
const { units } = useUnitsStore();
@@ -44,6 +64,223 @@ export default function Settings() {
return activeUnit?.Name || t('common.unknown');
}, [activeUnit, t]);
+ /**
+ * Clears all app data, cached values, settings, and stores
+ * Called when user confirms logout
+ */
+ const clearAllAppData = useCallback(async () => {
+ logger.info({
+ message: 'Clearing all app data on logout',
+ });
+
+ try {
+ // Clear persisted storage items
+ removeActiveUnitId();
+ removeActiveCallId();
+ removeDeviceUuid();
+
+ // Clear all MMKV storage except first time flag and user preferences
+ const allKeys = storage.getAllKeys();
+ const keysToPreserve = ['IS_FIRST_TIME'];
+ allKeys.forEach((key) => {
+ if (!keysToPreserve.includes(key)) {
+ storage.delete(key);
+ }
+ });
+
+ // Reset all zustand stores to their initial states
+ // Core stores
+ useCoreStore.setState({
+ activeUnitId: null,
+ activeUnit: null,
+ activeUnitStatus: null,
+ activeUnitStatusType: null,
+ activeCallId: null,
+ activeCall: null,
+ activePriority: null,
+ config: null,
+ isLoading: false,
+ isInitialized: false,
+ isInitializing: false,
+ error: null,
+ activeStatuses: null,
+ });
+
+ // Calls store
+ useCallsStore.setState({
+ calls: [],
+ callPriorities: [],
+ callTypes: [],
+ isLoading: false,
+ error: null,
+ });
+
+ // Units store
+ useUnitsStore.setState({
+ units: [],
+ unitStatuses: [],
+ isLoading: false,
+ error: null,
+ });
+
+ // Contacts store
+ useContactsStore.setState({
+ contacts: [],
+ contactNotes: {},
+ searchQuery: '',
+ selectedContactId: null,
+ isDetailsOpen: false,
+ isLoading: false,
+ isNotesLoading: false,
+ error: null,
+ });
+
+ // Notes store
+ useNotesStore.setState({
+ notes: [],
+ searchQuery: '',
+ selectedNoteId: null,
+ isDetailsOpen: false,
+ isLoading: false,
+ error: null,
+ });
+
+ // Roles store
+ useRolesStore.setState({
+ roles: [],
+ unitRoleAssignments: [],
+ users: [],
+ isLoading: false,
+ error: null,
+ });
+
+ // Protocols store
+ useProtocolsStore.setState({
+ protocols: [],
+ searchQuery: '',
+ selectedProtocolId: null,
+ isDetailsOpen: false,
+ isLoading: false,
+ error: null,
+ });
+
+ // Dispatch store
+ useDispatchStore.setState({
+ data: {
+ users: [],
+ groups: [],
+ roles: [],
+ units: [],
+ },
+ selection: {
+ everyone: false,
+ users: [],
+ groups: [],
+ roles: [],
+ units: [],
+ },
+ isLoading: false,
+ error: null,
+ searchQuery: '',
+ });
+
+ // Security store
+ securityStore.setState({
+ error: null,
+ rights: null,
+ });
+
+ // Status bottom sheet store
+ useStatusBottomSheetStore.getState().reset();
+
+ // Offline queue store
+ useOfflineQueueStore.getState().clearAllEvents();
+
+ // Loading store
+ useLoadingStore.getState().resetLoading();
+
+ // Location store
+ useLocationStore.setState({
+ latitude: null,
+ longitude: null,
+ heading: null,
+ accuracy: null,
+ speed: null,
+ altitude: null,
+ timestamp: null,
+ });
+
+ // LiveKit store - disconnect and reset
+ const liveKitState = useLiveKitStore.getState();
+ if (liveKitState.isConnected) {
+ liveKitState.disconnectFromRoom();
+ }
+ useLiveKitStore.setState({
+ isConnected: false,
+ isConnecting: false,
+ currentRoom: null,
+ currentRoomInfo: null,
+ isTalking: false,
+ availableRooms: [],
+ isBottomSheetVisible: false,
+ });
+
+ // Audio stream store - cleanup and reset
+ const audioStreamState = useAudioStreamStore.getState();
+ await audioStreamState.cleanup();
+ useAudioStreamStore.setState({
+ availableStreams: [],
+ currentStream: null,
+ isPlaying: false,
+ isLoading: false,
+ isBuffering: false,
+ isBottomSheetVisible: false,
+ });
+
+ // Bluetooth audio store
+ useBluetoothAudioStore.setState({
+ connectedDevice: null,
+ isScanning: false,
+ isConnecting: false,
+ availableDevices: [],
+ connectionError: null,
+ isAudioRoutingActive: false,
+ });
+
+ // Push notification modal store
+ usePushNotificationModalStore.setState({
+ isOpen: false,
+ notification: null,
+ });
+
+ logger.info({
+ message: 'Successfully cleared all app data',
+ });
+ } catch (error) {
+ logger.error({
+ message: 'Error clearing app data on logout',
+ context: { error },
+ });
+ }
+ }, []);
+
+ /**
+ * Handles logout confirmation - clears all data and signs out
+ */
+ const handleLogoutConfirm = useCallback(async () => {
+ setShowLogoutConfirm(false);
+
+ trackEvent('user_logout_confirmed', {
+ hadActiveUnit: !!activeUnit,
+ });
+
+ // Clear all app data first
+ await clearAllAppData();
+
+ // Then sign out
+ await signOut();
+ }, [clearAllAppData, signOut, trackEvent, activeUnit]);
+
const handleLoginInfoSubmit = async (data: { username: string; password: string }) => {
logger.info({
message: 'Updating login info',
@@ -89,7 +326,7 @@ export default function Settings() {
- setShowServerUrl(true)} textStyle="text-info-600" />
- setShowLoginInfo(true)} textStyle="text-info-600" />
- setShowUnitSelection(true)} textStyle="text-info-600" />
-
+ - setShowLogoutConfirm(true)} textStyle="text-error-600" />
@@ -122,6 +359,27 @@ export default function Settings() {
setShowLoginInfo(false)} onSubmit={handleLoginInfoSubmit} />
setShowServerUrl(false)} />
setShowUnitSelection(false)} />
+
+ {/* Logout Confirmation Dialog */}
+ setShowLogoutConfirm(false)}>
+
+
+
+ {t('settings.logout_confirm_title')}
+
+
+ {t('settings.logout_confirm_message')}
+
+
+
+
+
+
+
);
}
diff --git a/src/components/calls/__tests__/close-call-bottom-sheet.test.tsx b/src/components/calls/__tests__/close-call-bottom-sheet.test.tsx
index bf420759..e344f53a 100644
--- a/src/components/calls/__tests__/close-call-bottom-sheet.test.tsx
+++ b/src/components/calls/__tests__/close-call-bottom-sheet.test.tsx
@@ -40,6 +40,32 @@ jest.mock('nativewind', () => ({
// Mock cssInterop globally
(global as any).cssInterop = jest.fn();
+// Mock actionsheet components
+jest.mock('@/components/ui/actionsheet', () => ({
+ Actionsheet: ({ children, isOpen }: any) => {
+ const { View } = require('react-native');
+ return isOpen ? {children} : null;
+ },
+ ActionsheetBackdrop: () => null,
+ ActionsheetContent: ({ children }: any) => {
+ const { View } = require('react-native');
+ return {children};
+ },
+ ActionsheetDragIndicator: () => null,
+ ActionsheetDragIndicatorWrapper: ({ children }: any) => {
+ const { View } = require('react-native');
+ return {children};
+ },
+}));
+
+// Mock keyboard aware scroll view
+jest.mock('react-native-keyboard-controller', () => ({
+ KeyboardAwareScrollView: ({ children }: any) => {
+ const { View } = require('react-native');
+ return {children};
+ },
+}));
+
// Mock UI components
jest.mock('@/components/ui/bottom-sheet', () => ({
CustomBottomSheet: ({ children, isOpen }: any) => {
diff --git a/src/components/calls/close-call-bottom-sheet.tsx b/src/components/calls/close-call-bottom-sheet.tsx
index 1ae40f6c..ccf7d8a9 100644
--- a/src/components/calls/close-call-bottom-sheet.tsx
+++ b/src/components/calls/close-call-bottom-sheet.tsx
@@ -1,9 +1,9 @@
import { useRouter } from 'expo-router';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { useWindowDimensions } from 'react-native';
+import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
-import { CustomBottomSheet } from '@/components/ui/bottom-sheet';
+import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetDragIndicator, ActionsheetDragIndicatorWrapper } from '@/components/ui/actionsheet';
import { Button, ButtonText } from '@/components/ui/button';
import { FormControl, FormControlLabel, FormControlLabelText } from '@/components/ui/form-control';
import { HStack } from '@/components/ui/hstack';
@@ -26,8 +26,6 @@ interface CloseCallBottomSheetProps {
export const CloseCallBottomSheet: React.FC = ({ isOpen, onClose, callId, isLoading = false }) => {
const { t } = useTranslation();
const router = useRouter();
- const { width, height } = useWindowDimensions();
- const isLandscape = width > height;
const showToast = useToastStore((state) => state.showToast);
const { trackEvent } = useAnalytics();
const { closeCall } = useCallDetailStore();
@@ -90,52 +88,59 @@ export const CloseCallBottomSheet: React.FC = ({ isOp
const isButtonDisabled = isLoading || isSubmitting;
return (
-
-
- {t('call_detail.close_call')}
-
-
-
- {t('call_detail.close_call_type')}
-
-
-
-
-
-
- {t('call_detail.close_call_note')}
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ {t('call_detail.close_call')}
+
+
+
+ {t('call_detail.close_call_type')}
+
+
+
+
+
+ {t('call_detail.close_call_note')}
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/src/components/contacts/contact-details-sheet.tsx b/src/components/contacts/contact-details-sheet.tsx
index 2d7d45dd..e05373b4 100644
--- a/src/components/contacts/contact-details-sheet.tsx
+++ b/src/components/contacts/contact-details-sheet.tsx
@@ -16,9 +16,9 @@ import {
UserIcon,
X,
} from 'lucide-react-native';
-import React, { useState } from 'react';
+import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { ScrollView, useWindowDimensions, View } from 'react-native';
+import { Linking, Platform, ScrollView, useWindowDimensions, View } from 'react-native';
import { Avatar, AvatarImage } from '@/components/ui/avatar';
import { useAnalytics } from '@/hooks/use-analytics';
@@ -59,28 +59,79 @@ const Section: React.FC = ({ title, icon, children, isCollapsible
);
};
+type ContactFieldAction = 'email' | 'phone' | 'address' | 'website' | 'none';
+
interface ContactFieldProps {
label: string;
value: string | null | undefined;
icon?: React.ReactNode;
isLink?: boolean;
linkPrefix?: string;
+ action?: ContactFieldAction;
+ fullAddress?: string;
}
-const ContactField: React.FC = ({ label, value, icon, isLink, linkPrefix }) => {
+const ContactField: React.FC = ({ label, value, icon, isLink, linkPrefix, action = 'none', fullAddress }) => {
+ const handlePress = useCallback(async () => {
+ if (action === 'none' || !value) return;
+
+ let url: string | null = null;
+
+ switch (action) {
+ case 'email':
+ url = `mailto:${value}`;
+ break;
+ case 'phone':
+ url = `tel:${value.toString().replace(/[^0-9+]/g, '')}`;
+ break;
+ case 'address': {
+ const addressToOpen = fullAddress || value.toString();
+ const encodedAddress = encodeURIComponent(addressToOpen);
+ url = Platform.select({
+ ios: `maps:0,0?q=${encodedAddress}`,
+ android: `geo:0,0?q=${encodedAddress}`,
+ default: `https://maps.google.com/?q=${encodedAddress}`,
+ });
+ break;
+ }
+ case 'website': {
+ const websiteUrl = value.toString();
+ url = websiteUrl.startsWith('http') ? websiteUrl : `https://${websiteUrl}`;
+ break;
+ }
+ }
+
+ if (url) {
+ const canOpen = await Linking.canOpenURL(url);
+ if (canOpen) {
+ await Linking.openURL(url);
+ }
+ }
+ }, [action, value, fullAddress]);
+
if (!value || value.toString().trim() === '') return null;
const displayValue = isLink && linkPrefix ? `${linkPrefix}${value}` : value.toString();
- return (
+ const isActionable = action !== 'none';
+
+ const content = (
{icon ? {icon} : null}
{label}
- {displayValue}
+ {displayValue}
);
+
+ return isActionable ? (
+
+ {content}
+
+ ) : (
+ content
+ );
};
export const ContactDetailsSheet: React.FC = () => {
@@ -226,13 +277,13 @@ export const ContactDetailsSheet: React.FC = () => {
{hasContactInfo ? (
}>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
+ } action="email" />
+ } action="phone" />
+ } action="phone" />
+ } action="phone" />
+ } action="phone" />
+ } action="phone" />
+ } action="phone" />
) : null}
@@ -241,17 +292,25 @@ export const ContactDetailsSheet: React.FC = () => {
{hasLocationInfo ? (
}>
- } />
+ }
+ action="address"
+ fullAddress={[selectedContact.Address, selectedContact.City, selectedContact.State, selectedContact.Zip].filter(Boolean).join(', ')}
+ />
{selectedContact.City || selectedContact.State || selectedContact.Zip ? (
}
+ action="address"
+ fullAddress={[selectedContact.Address, selectedContact.City, selectedContact.State, selectedContact.Zip].filter(Boolean).join(', ')}
/>
) : null}
- } />
- } />
- } />
+ } action="address" />
+ } action="address" />
+ } action="address" />
) : null}
@@ -260,7 +319,7 @@ export const ContactDetailsSheet: React.FC = () => {
{hasSocialMedia ? (
} defaultExpanded={false}>
- } isLink />
+ } isLink action="website" />
diff --git a/src/components/protocols/__tests__/protocol-card.test.tsx b/src/components/protocols/__tests__/protocol-card.test.tsx
index 3fda919d..8f98fb86 100644
--- a/src/components/protocols/__tests__/protocol-card.test.tsx
+++ b/src/components/protocols/__tests__/protocol-card.test.tsx
@@ -20,7 +20,7 @@ describe('ProtocolCard', () => {
});
const baseProtocol: CallProtocolsResultData = {
- Id: '1',
+ ProtocolId: '1',
DepartmentId: 'dept1',
Name: 'Fire Emergency Response',
Code: 'FIRE001',
@@ -39,7 +39,7 @@ describe('ProtocolCard', () => {
};
const protocolWithoutOptionalFields: CallProtocolsResultData = {
- Id: '2',
+ ProtocolId: '2',
DepartmentId: 'dept1',
Name: 'Basic Protocol',
Code: '',
@@ -58,7 +58,7 @@ describe('ProtocolCard', () => {
};
const protocolWithHtmlDescription: CallProtocolsResultData = {
- Id: '3',
+ ProtocolId: '3',
DepartmentId: 'dept1',
Name: 'Protocol with HTML',
Code: 'HTML001',
@@ -220,7 +220,7 @@ describe('ProtocolCard', () => {
describe('Edge Cases', () => {
it('should handle protocol with empty ID', () => {
- const protocolWithEmptyId = { ...baseProtocol, Id: '' };
+ const protocolWithEmptyId = { ...baseProtocol, ProtocolId: '' };
render();
const card = screen.getByText('Fire Emergency Response');
diff --git a/src/components/protocols/__tests__/protocol-details-sheet.test.tsx b/src/components/protocols/__tests__/protocol-details-sheet.test.tsx
index 86f3c650..b4e28836 100644
--- a/src/components/protocols/__tests__/protocol-details-sheet.test.tsx
+++ b/src/components/protocols/__tests__/protocol-details-sheet.test.tsx
@@ -76,7 +76,7 @@ jest.mock('@/components/ui/actionsheet', () => ({
// Mock protocols test data
const mockProtocols: CallProtocolsResultData[] = [
{
- Id: '1',
+ ProtocolId: '1',
DepartmentId: 'dept1',
Name: 'Fire Emergency Response',
Code: 'FIRE001',
@@ -94,7 +94,7 @@ const mockProtocols: CallProtocolsResultData[] = [
Questions: [],
},
{
- Id: '2',
+ ProtocolId: '2',
DepartmentId: 'dept1',
Name: 'Basic Protocol',
Code: '',
@@ -413,7 +413,7 @@ describe('ProtocolDetailsSheet', () => {
it('should track analytics event with false flags when protocol has no optional data', () => {
const minimalProtocol: CallProtocolsResultData = {
- Id: 'protocol-minimal',
+ ProtocolId: 'protocol-minimal',
DepartmentId: 'dept1',
Name: 'Minimal Protocol',
Code: '',
@@ -513,7 +513,7 @@ describe('ProtocolDetailsSheet', () => {
it('should track analytics event when selected protocol changes', () => {
const secondProtocol: CallProtocolsResultData = {
...mockProtocols[0],
- Id: '3',
+ ProtocolId: '3',
Name: 'Second Protocol',
Code: 'SP002',
};
diff --git a/src/components/protocols/protocol-card.tsx b/src/components/protocols/protocol-card.tsx
index e9eb4aaf..48298dc7 100644
--- a/src/components/protocols/protocol-card.tsx
+++ b/src/components/protocols/protocol-card.tsx
@@ -16,7 +16,7 @@ interface ProtocolCardProps {
export const ProtocolCard: React.FC = ({ protocol, onPress }) => {
return (
- onPress(protocol.Id)} testID={`protocol-card-${protocol.Id}`}>
+ onPress(protocol.ProtocolId)} testID={`protocol-card-${protocol.ProtocolId}`}>
{protocol.Name}
diff --git a/src/components/protocols/protocol-details-sheet.tsx b/src/components/protocols/protocol-details-sheet.tsx
index 8ec5eb1e..793583b7 100644
--- a/src/components/protocols/protocol-details-sheet.tsx
+++ b/src/components/protocols/protocol-details-sheet.tsx
@@ -22,13 +22,13 @@ export const ProtocolDetailsSheet: React.FC = () => {
const { trackEvent } = useAnalytics();
const { protocols, selectedProtocolId, isDetailsOpen, closeDetails } = useProtocolsStore();
- const selectedProtocol = protocols.find((protocol) => protocol.Id === selectedProtocolId);
+ const selectedProtocol = protocols.find((protocol) => protocol.ProtocolId === selectedProtocolId);
// Track when protocol details sheet is opened/rendered
useEffect(() => {
if (isDetailsOpen && selectedProtocol) {
trackEvent('protocol_details_sheet_opened', {
- protocolId: selectedProtocol.Id,
+ protocolId: selectedProtocol.ProtocolId,
protocolName: selectedProtocol.Name,
hasCode: !!selectedProtocol.Code,
hasDescription: !!selectedProtocol.Description,
@@ -38,14 +38,21 @@ export const ProtocolDetailsSheet: React.FC = () => {
}
}, [isDetailsOpen, selectedProtocol, trackEvent]);
- if (!selectedProtocol) return null;
+ if (!selectedProtocol) {
+ return (
+
+
+
+
+ );
+ }
const textColor = colorScheme === 'dark' ? '#E5E7EB' : '#1F2937'; // gray-200 : gray-800
return (
-
+
@@ -79,6 +86,7 @@ export const ProtocolDetailsSheet: React.FC = () => {
{/* Protocol content in WebView */}
-
- {isLoadingUnits ? (
-
+ {isLoading ? (
+
+
-
- ) : units.length > 0 ? (
-
- {units.map((unit) => (
- handleUnitSelection(unit)}
- disabled={isLoading}
- className={activeUnit?.UnitId === unit.UnitId ? 'data-[checked=true]:bg-background-100' : ''}
- testID={`unit-item-${unit.UnitId}`}
- >
-
-
- {unit.Name}
-
-
- {unit.Type}
-
-
- {activeUnit?.UnitId === unit.UnitId && }
-
- ))}
+ {t('settings.activating_unit')}
- ) : (
-
- {t('settings.no_units_available')}
-
- )}
-
+
+ ) : (
+
+ {isLoadingUnits ? (
+
+
+
+ ) : units.length > 0 ? (
+
+ {units.map((unit) => (
+ handleUnitSelection(unit)}
+ disabled={isLoading}
+ className={activeUnit?.UnitId === unit.UnitId ? 'data-[checked=true]:bg-background-100' : ''}
+ testID={`unit-item-${unit.UnitId}`}
+ >
+
+
+ {unit.Name}
+
+
+ {unit.Type}
+
+
+ {activeUnit?.UnitId === unit.UnitId && }
+
+ ))}
+
+ ) : (
+
+ {t('settings.no_units_available')}
+
+ )}
+
+ )}
{/* Cancel Button - Fixed to bottom */}
diff --git a/src/components/sidebar/sidebar.tsx b/src/components/sidebar/sidebar.tsx
index 1352543c..38b2e9de 100644
--- a/src/components/sidebar/sidebar.tsx
+++ b/src/components/sidebar/sidebar.tsx
@@ -67,7 +67,7 @@ const Sidebar = () => {
))}
diff --git a/src/components/status/status-bottom-sheet.tsx b/src/components/status/status-bottom-sheet.tsx
index 332bbd4f..f0b24174 100644
--- a/src/components/status/status-bottom-sheet.tsx
+++ b/src/components/status/status-bottom-sheet.tsx
@@ -3,6 +3,7 @@ import { useColorScheme } from 'nativewind';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView, TouchableOpacity } from 'react-native';
+import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
import { invertColor } from '@/lib/utils';
import { type CustomStatusResultData } from '@/models/v4/customStatuses/customStatusResultData';
@@ -504,7 +505,10 @@ export const StatusBottomSheet = () => {
-
+
+