Skip to content

Commit 7a3d73f

Browse files
committed
RD-T42 PR#113 fixes
1 parent 0ae7090 commit 7a3d73f

11 files changed

Lines changed: 343 additions & 69 deletions

File tree

src/app/(app)/home.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,19 @@ export default function DispatchConsole() {
128128
}
129129
};
130130

131-
// Track analytics when view becomes visible
131+
// Refresh data and track analytics when view becomes visible
132132
useFocusEffect(
133133
useCallback(() => {
134134
trackEvent('dispatch_console_viewed', {
135135
timestamp: new Date().toISOString(),
136136
isLandscape,
137137
isTablet,
138138
});
139-
}, [trackEvent, isLandscape, isTablet])
139+
// Re-fetch core data so downstream panels (including check-in timers) refresh
140+
fetchCalls();
141+
fetchUnits();
142+
fetchPersonnel();
143+
}, [trackEvent, isLandscape, isTablet, fetchCalls, fetchUnits, fetchPersonnel])
140144
);
141145

142146
// Listen for SignalR personnel updates

src/app/weather-alerts/[id].tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next';
66
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, View } from 'react-native';
77

88
import { HStack } from '@/components/ui/hstack';
9-
import { Icon } from '@/components/ui/icon';
109
import { Text } from '@/components/ui/text';
1110
import { VStack } from '@/components/ui/vstack';
1211
import { SEVERITY_COLORS, WeatherAlertCategory, WeatherAlertCertainty, WeatherAlertSeverity, WeatherAlertUrgency } from '@/models/v4/weatherAlerts/weatherAlertEnums';
@@ -134,7 +133,7 @@ export default function WeatherAlertDetailScreen() {
134133
{/* Header */}
135134
<View style={StyleSheet.flatten([styles.header, { backgroundColor: severityColor }])}>
136135
<HStack className="items-center" space="sm">
137-
<Icon as={CategoryIcon} size="md" color="#FFFFFF" />
136+
<CategoryIcon size={20} color="#FFFFFF" />
138137
<VStack>
139138
<Text className="text-lg font-bold text-white">{alert.Event}</Text>
140139
<Text className="text-xs font-semibold uppercase tracking-wider text-white/85">{getSeverityLabel(alert.Severity, t)}</Text>

src/components/calls/__tests__/call-files-modal.test.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ jest.mock('@/hooks/use-analytics', () => ({
5555
}),
5656
}));
5757

58-
// Mock expo modules
59-
jest.mock('expo-file-system', () => ({
58+
// Mock expo modules — component imports from 'expo-file-system/legacy'
59+
jest.mock('expo-file-system/legacy', () => ({
6060
documentDirectory: '/mock/documents/',
6161
writeAsStringAsync: jest.fn(),
6262
EncodingType: {
@@ -90,14 +90,13 @@ Object.defineProperty(global, 'FileReader', {
9090
result: string | ArrayBuffer | null = null;
9191
readyState = 0;
9292
onload: ((event: any) => void) | null = null;
93+
onerror: ((event: any) => void) | null = null;
9394

94-
readAsDataURL(blob: Blob) {
95-
// Simulate successful file read
96-
setTimeout(() => {
97-
this.result = 'data:application/pdf;base64,dGVzdCBjb250ZW50'; // base64 for "test content"
98-
this.readyState = 2; // DONE
99-
if (this.onload) this.onload(new Event('load') as any);
100-
}, 0);
95+
readAsDataURL(_blob: Blob) {
96+
// Fire synchronously — onload is set before readAsDataURL is called
97+
this.result = 'data:application/pdf;base64,dGVzdCBjb250ZW50'; // base64 for "test content"
98+
this.readyState = 2; // DONE
99+
if (this.onload) this.onload(new Event('load') as any);
101100
}
102101
}
103102
});
@@ -492,7 +491,7 @@ describe('CallFilesModal', () => {
492491

493492
describe('File Download', () => {
494493
const mockGetCallAttachmentFile = require('@/api/calls/callFiles').getCallAttachmentFile;
495-
const mockWriteAsStringAsync = require('expo-file-system').writeAsStringAsync;
494+
const mockWriteAsStringAsync = require('expo-file-system/legacy').writeAsStringAsync;
496495
const mockShareAsync = require('expo-sharing').shareAsync;
497496

498497
beforeEach(() => {

src/components/checkIn/check-in-bottom-sheet.tsx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,41 @@ import { VStack } from '@/components/ui/vstack';
1212
import { type CheckInTimerStatusResultData } from '@/models/v4/checkIn/checkInTimerStatusResultData';
1313
import { useLocationStore } from '@/stores/app/location-store';
1414
import { useCheckInStore } from '@/stores/checkIn/store';
15+
import { usePersonnelStore } from '@/stores/personnel/store';
1516
import { useToastStore } from '@/stores/toast/store';
17+
import { useUnitsStore } from '@/stores/units/store';
18+
19+
/** Resolve the display name for a check-in timer entity */
20+
function resolveTimerDisplayName(
21+
timer: CheckInTimerStatusResultData,
22+
units: { UnitId: string; Name: string }[],
23+
personnel: { UserId: string; IdentificationNumber: string; FirstName: string; LastName: string }[],
24+
fallback: string
25+
): string {
26+
// TargetName from API is often the type label (e.g. "UnitType"), not the entity name
27+
if (timer.TargetName && !/type$/i.test(timer.TargetName) && timer.TargetName !== timer.TargetTypeName) {
28+
return timer.TargetName;
29+
}
30+
31+
const entityId = timer.TargetEntityId;
32+
const unitId = timer.UnitId;
33+
34+
for (const u of units) {
35+
if (entityId && u.UnitId === entityId) return u.Name;
36+
if (unitId > 0 && u.UnitId === String(unitId)) return u.Name;
37+
if (unitId > 0 && parseInt(u.UnitId, 10) === unitId) return u.Name;
38+
}
39+
40+
if (entityId) {
41+
for (const p of personnel) {
42+
if (p.UserId === entityId || p.IdentificationNumber === entityId) {
43+
return `${p.FirstName} ${p.LastName}`.trim();
44+
}
45+
}
46+
}
47+
48+
return entityId || fallback;
49+
}
1650

1751
const CHECK_IN_TYPE_KEYS: Record<string, string> = {
1852
Personnel: 'check_in.type_personnel',
@@ -38,6 +72,8 @@ export const CheckInBottomSheet: React.FC<CheckInBottomSheetProps> = ({ isOpen,
3872
const isLandscape = width > height;
3973
const showToast = useToastStore((state) => state.showToast);
4074
const { performCheckIn, isCheckingIn } = useCheckInStore();
75+
const units = useUnitsStore((s) => s.units);
76+
const personnel = usePersonnelStore((s) => s.personnel);
4177
const userLocation = useLocationStore((state) => ({
4278
latitude: state.latitude,
4379
longitude: state.longitude,
@@ -135,7 +171,7 @@ export const CheckInBottomSheet: React.FC<CheckInBottomSheetProps> = ({ isOpen,
135171
{timers.map((timer) => (
136172
<Button key={`${timer.TargetType}-${timer.TargetEntityId}`} variant="outline" onPress={() => handleSelectTarget(timer)} className="w-full justify-start" size={isLandscape ? 'md' : 'sm'}>
137173
<VStack className="items-start">
138-
<ButtonText className={`font-bold ${isLandscape ? '' : 'text-xs'}`}>{timer.TargetName || timer.TargetEntityId || typeLabel(timer)}</ButtonText>
174+
<ButtonText className={`font-bold ${isLandscape ? '' : 'text-xs'}`}>{resolveTimerDisplayName(timer, units, personnel, typeLabel(timer))}</ButtonText>
139175
<Text className="text-xs text-gray-500">{typeLabel(timer)}</Text>
140176
</VStack>
141177
</Button>
@@ -146,7 +182,7 @@ export const CheckInBottomSheet: React.FC<CheckInBottomSheetProps> = ({ isOpen,
146182
{step === 'confirm' && selected && (
147183
<VStack className="gap-4">
148184
<VStack>
149-
<Text className="text-base font-bold">{selected.TargetName || selected.TargetEntityId || typeLabel(selected)}</Text>
185+
<Text className="text-base font-bold">{resolveTimerDisplayName(selected, units, personnel, typeLabel(selected))}</Text>
150186
<Text className="text-sm text-gray-500">{typeLabel(selected)}</Text>
151187
</VStack>
152188

src/components/checkIn/check-in-timer-card.tsx

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ShieldCheckIcon } from 'lucide-react-native';
22
import { useColorScheme } from 'nativewind';
3-
import React from 'react';
3+
import React, { useMemo } from 'react';
44
import { useTranslation } from 'react-i18next';
55
import { View } from 'react-native';
66

@@ -10,6 +10,8 @@ import { HStack } from '@/components/ui/hstack';
1010
import { Text } from '@/components/ui/text';
1111
import { VStack } from '@/components/ui/vstack';
1212
import { type CheckInTimerStatusResultData } from '@/models/v4/checkIn/checkInTimerStatusResultData';
13+
import { usePersonnelStore } from '@/stores/personnel/store';
14+
import { useUnitsStore } from '@/stores/units/store';
1315

1416
const STATUS_COLORS: Record<string, string> = {
1517
Ok: '#22C55E',
@@ -39,13 +41,54 @@ interface CheckInTimerCardProps {
3941
export const CheckInTimerCard: React.FC<CheckInTimerCardProps> = React.memo(({ timer, onCheckIn }) => {
4042
const { t } = useTranslation();
4143
const { colorScheme } = useColorScheme();
44+
const units = useUnitsStore((s) => s.units);
45+
const personnel = usePersonnelStore((s) => s.personnel);
4246

4347
const statusColor = STATUS_COLORS[timer.Status] || '#6B7280';
4448
const progress = timer.DurationMinutes > 0 ? Math.min((timer.ElapsedMinutes / timer.DurationMinutes) * 100, 100) : 0;
4549

4650
// Use TargetTypeName from API, fall back to translation key lookup
4751
const typeLabel = timer.TargetTypeName || (CHECK_IN_TYPE_KEYS[String(timer.TargetType)] ? t(CHECK_IN_TYPE_KEYS[String(timer.TargetType)]) : String(timer.TargetType));
4852

53+
// The API's TargetName often contains the check-in TYPE name (e.g. "UnitType")
54+
// rather than the actual entity name (e.g. "Engine 1"). Detect this and look up
55+
// the real name from the units/personnel stores using TargetEntityId.
56+
const isTargetNameActuallyTypeName = !timer.TargetName
57+
|| timer.TargetName === timer.TargetTypeName
58+
|| /type$/i.test(timer.TargetName)
59+
|| Object.keys(CHECK_IN_TYPE_KEYS).some((k) => k.toLowerCase() === timer.TargetName.toLowerCase())
60+
|| timer.TargetName.toLowerCase() === 'unittype'
61+
|| timer.TargetName.toLowerCase() === 'personneltype';
62+
63+
const displayName = useMemo(() => {
64+
// If TargetName is a real entity name (not a type label), use it directly
65+
if (timer.TargetName && !isTargetNameActuallyTypeName) return timer.TargetName;
66+
67+
const entityId = timer.TargetEntityId;
68+
const unitId = timer.UnitId;
69+
70+
// Look up from units store by TargetEntityId or UnitId
71+
if (units.length > 0) {
72+
for (const u of units) {
73+
if (entityId && u.UnitId === entityId) return u.Name;
74+
if (unitId > 0 && u.UnitId === String(unitId)) return u.Name;
75+
if (unitId > 0 && parseInt(u.UnitId, 10) === unitId) return u.Name;
76+
if (entityId && !isNaN(parseInt(entityId, 10)) && parseInt(u.UnitId, 10) === parseInt(entityId, 10)) return u.Name;
77+
}
78+
}
79+
80+
// Look up from personnel store by TargetEntityId
81+
if (personnel.length > 0 && entityId) {
82+
for (const p of personnel) {
83+
if (p.UserId === entityId || p.IdentificationNumber === entityId) {
84+
return `${p.FirstName} ${p.LastName}`.trim();
85+
}
86+
}
87+
}
88+
89+
return entityId || typeLabel;
90+
}, [timer.TargetName, timer.TargetEntityId, timer.UnitId, isTargetNameActuallyTypeName, units, personnel, typeLabel]);
91+
4992
const statusKey = `check_in.status_${timer.Status.toLowerCase()}` as const;
5093

5194
const elapsedText = timer.LastCheckIn ? t('check_in.minutes_ago', { count: Math.round(timer.ElapsedMinutes) }) : '';
@@ -57,15 +100,8 @@ export const CheckInTimerCard: React.FC<CheckInTimerCardProps> = React.memo(({ t
57100
<HStack className="flex-1 items-center gap-2">
58101
<ShieldCheckIcon size={16} color={statusColor} />
59102
<VStack className="ml-2 flex-1">
60-
<Text className="text-sm font-bold">{timer.TargetName || timer.TargetEntityId || typeLabel}</Text>
61-
{timer.TargetName ? (
62-
<Text className="text-xs font-medium text-gray-500">{typeLabel}</Text>
63-
) : (
64-
<Text className="text-xs font-medium text-gray-500">
65-
{timer.TargetTypeName || String(timer.TargetType)}
66-
{timer.TargetEntityId ? ` #${timer.TargetEntityId}` : ''}
67-
</Text>
68-
)}
103+
<Text className="text-sm font-bold">{displayName}</Text>
104+
<Text className="text-xs font-medium text-gray-500">{typeLabel}</Text>
69105
</VStack>
70106
</HStack>
71107
<Box className="rounded-full px-2 py-0.5" style={{ backgroundColor: statusColor + '20' }}>

0 commit comments

Comments
 (0)