Incident details encoding and UI headers

Introduces Unicode-safe base64 encoding/decoding for call details between incidents and call screens. Refactors PageHeader to accept left, center, and right header props for more flexible layouts. Updates call.jsx and register.jsx to use the new header structure, and improves department/unit rendering and modal dropdowns for department selection. Generalizes and modernizes UI code for better maintainability and cross-platform compatibility.
This commit is contained in:
Matt DiMeglio 2025-08-12 16:36:26 -04:00
parent 2f01c575db
commit 1b8533d192
8 changed files with 939 additions and 909 deletions

View file

@ -1,5 +1,6 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { router } from 'expo-router';
import { useCallFeed } from '../hooks/useCallFeed'; import { useCallFeed } from '../hooks/useCallFeed';
import { Platform, Linking, View, ScrollView, Text, TouchableOpacity } from 'react-native'; import { Platform, Linking, View, ScrollView, Text, TouchableOpacity } from 'react-native';
import { useLocalSearchParams } from 'expo-router'; import { useLocalSearchParams } from 'expo-router';
@ -16,10 +17,14 @@ import ActionSheet from 'react-native-actions-sheet';
const DepartmentActionSheet = styled(ActionSheet)``; const DepartmentActionSheet = styled(ActionSheet)``;
function fromBase64Unicode(str) {
return new TextDecoder().decode(Uint8Array.from(atob(str), c => c.charCodeAt(0)));
}
export default function Call() { export default function Call() {
const { callDetails } = useLocalSearchParams(); const { callDetails } = useLocalSearchParams();
const actionSheetRef = useRef(null); const actionSheetRef = useRef(null);
const callFeed = useCallFeed(); const callFeed = useCallFeed(true);
const { const {
departments, departments,
@ -39,56 +44,56 @@ export default function Call() {
deptList, deptList,
} = departments; } = departments;
const decoded = atob(callDetails); const decoded = fromBase64Unicode(callDetails);
const { Incident, Address, Person, Response } = JSON.parse(decoded); const { incident, address, person, response } = JSON.parse(decoded);
const { CallThemes } = accountDetails; const { CallThemes } = accountDetails;
const { const {
IncID, incID,
IncNumber, incNumber,
JurisdictionNumber, jurisdictionNumber,
ServiceNumber, serviceNumber,
ServiceID, serviceID,
IncDate, incDate,
IncNature, incNature,
IncNatureCode, incNatureCode,
IncNatureCodeDesc, incNatureCodeDesc,
Notes, notes,
Status, status,
Origin, origin,
} = Incident; } = incident;
const { const {
StreetAddress, streetAddress,
AddressApartment, addressApartment,
Town, town,
State, state,
ZipCode, zipCode,
Latitude, latitude,
Longitude, longitude,
County, county,
Intersection1, intersection1,
Intersection2, intersection2,
LocationName, locationName,
WeatherCondition, weatherCondition,
} = Address; } = address;
const { const {
Name, name,
Age, age,
Gender, gender,
Statement, statement,
Conscious, conscious,
Breathing, breathing,
CallBackNumber, callBackNumber,
} = Person; } = person;
const { const {
Units units
} = Response; } = response;
const SelectedIcon = callIconMap[IncNature] || AccidentAndEmergency; const SelectedIcon = callIconMap[incNature] || AccidentAndEmergency;
const ownDepartmentResponse = Units?.map((unit) => { const ownDepartmentResponse = units?.map((unit) => {
if (unit?.Department === selectedDepartment?.dept || if (unit?.department === selectedDepartment?.dept ||
selectedDepartment?.addDepts?.includes(unit?.Department)) { selectedDepartment?.addDepts?.includes(unit?.department)) {
return unit; return unit;
} }
return null; return null;
@ -96,9 +101,9 @@ export default function Call() {
return filterItem; return filterItem;
}); });
const mutualAidDepartmentResponse = Units?.map((unit) => { const mutualAidDepartmentResponse = units?.map((unit) => {
if (unit?.Department !== selectedDepartment?.dept && if (unit?.department !== selectedDepartment?.dept &&
!selectedDepartment?.addDepts?.includes(unit?.Department)) { !selectedDepartment?.addDepts?.includes(unit?.department)) {
return unit; return unit;
} }
return null; return null;
@ -107,7 +112,7 @@ export default function Call() {
});; });;
const formatResponseTimes = (time) => { const formatResponseTimes = (time) => {
if (time === null) { if (time === null || time === undefined || time === '') {
return ''; return '';
} }
const initTime = new Date(time); const initTime = new Date(time);
@ -129,16 +134,17 @@ export default function Call() {
return ( return (
<React.Fragment> <React.Fragment>
<PageHeader> <PageHeader
<View leftHeader={ <View style={{ flex: 1, alignItems: 'flex-start' }}>
style={{ <TouchableOpacity
flexDirection: 'column', onPress={router.back}
height: 80, style={{ flexDirection: 'row', alignItems: 'center' }}
alignItems: 'center',
justifyContent: 'flex-end',
paddingBottom: 7
}}
> >
<Ionicons name="chevron-back-outline" size={22} color="red" style={{ paddingLeft: 10 }} />
<Text style={{ color: 'red', fontWeight: 600 }}>Back to Incidents</Text>
</TouchableOpacity>
</View>}
centerHeader={<View style={{ flex: 1, alignItems: 'center' }}>
<TouchableOpacity <TouchableOpacity
style={{ style={{
borderRadius: 6, borderRadius: 6,
@ -159,15 +165,15 @@ export default function Call() {
style={{ style={{
color: selectedDepartmentColorPicker(selectedDepartment?.type), color: selectedDepartmentColorPicker(selectedDepartment?.type),
fontWeight: 600, fontWeight: 600,
fontSize: '14' fontSize: 14
}} }}
> >
{selectedDepartment?.deptAbv} {selectedDepartment?.deptAbv}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>}
</PageHeader> />
<ScrollView> <ScrollView showsVerticalScrollIndicator={false}>
<StatusBar style="dark" /> <StatusBar style="dark" />
<SafeAreaView /> <SafeAreaView />
<View style={{ flexDirection: 'column', padding: 20 }}> <View style={{ flexDirection: 'column', padding: 20 }}>
@ -180,8 +186,8 @@ export default function Call() {
justifyContent: 'space-between' justifyContent: 'space-between'
}} }}
> >
<Text style={{ fontSize: 12 }}>{formatCallDateTime(IncDate)}</Text> <Text style={{ fontSize: 12 }}>{formatCallDateTime(incDate)}</Text>
<Text style={{ fontSize: 12 }}>{formatCallTimePast(IncDate)}</Text> <Text style={{ fontSize: 12 }}>{formatCallTimePast(incDate)}</Text>
</View> </View>
<View key="callDetails" style={{ padding: 2 }}> <View key="callDetails" style={{ padding: 2 }}>
<View <View
@ -191,9 +197,9 @@ export default function Call() {
backgroundColor: '#fff', backgroundColor: '#fff',
shadowOffset: { width: 0, height: 0 }, shadowOffset: { width: 0, height: 0 },
shadowColor: callColorSelector( shadowColor: callColorSelector(
IncNatureCode, incNatureCode,
IncNature, incNature,
Status status
), ),
shadowOpacity: 1, shadowOpacity: 1,
shadowRadius: 5, shadowRadius: 5,
@ -201,18 +207,18 @@ export default function Call() {
}} }}
> >
<View style={{ flexDirection: 'column' }}> <View style={{ flexDirection: 'column' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}> <View style={{ flexDirection: 'row', alignItems: 'center', maxWidth: '90%' }}>
<SelectedIcon <SelectedIcon
color={callColorSelector( color={callColorSelector(
IncNatureCode, incNatureCode,
IncNature, incNature,
Status status
)} )}
opacity={0.3} opacity={0.3}
width={56} width={56}
height={56} height={56}
/> />
<View style={{ flexDirection: 'column' }}> <View style={{ flexDirection: 'column', maxWidth: '90%' }}>
<Text <Text
style={{ style={{
color: 'black', color: 'black',
@ -220,28 +226,28 @@ export default function Call() {
fontSize: 16 fontSize: 16
}} }}
> >
{`${IncNature}`} {`${incNature}`}
</Text> </Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', maxWidth: '90%' }}>
<Text <Text
style={{ style={{
color: 'black', color: 'black',
fontSize: 12, fontSize: 12,
textShadowColor: callColorSelector( textShadowColor: callColorSelector(
IncNatureCode, incNatureCode,
IncNature, incNature,
Status status
), ),
textShadowRadius: 1 textShadowRadius: 1
}} }}
> >
{`${IncNatureCodeDesc}`} {`${incNatureCodeDesc}`}
</Text> </Text>
</View> </View>
</View> </View>
</View> </View>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
{Status === 'CLOSED' ? ( {status.toLowerCase() === 'closed' ? (
<Text <Text
style={{ style={{
fontSize: 12, fontSize: 12,
@ -251,20 +257,20 @@ export default function Call() {
> >
This Incident is Closed. This Incident is Closed.
</Text> </Text>
) : <Text></Text> } ) : <Text></Text>}
<Text <Text
style={{ style={{
fontSize: 12, fontSize: 12,
textAlign: 'right' textAlign: 'right'
}} }}
> >
{`Incident #: ${ServiceNumber}`} {`Incident #: ${serviceNumber}`}
</Text> </Text>
</View> </View>
</View> </View>
</View> </View>
</View> </View>
<View style={{ margin: 6 }}/> <View style={{ margin: 6 }} />
<View key="callLocation" style={{ padding: 2 }}> <View key="callLocation" style={{ padding: 2 }}>
<View <View
style={{ style={{
@ -285,8 +291,8 @@ export default function Call() {
justifyContent: 'space-between' justifyContent: 'space-between'
}} }}
> >
<View style={{ flexDirection: 'column' }}> <View style={{ flexDirection: 'column', maxWidth: '70%' }}>
{LocationName ? ( {locationName ? (
<Text <Text
style={{ style={{
color: 'black', color: 'black',
@ -294,38 +300,38 @@ export default function Call() {
fontSize: 16 fontSize: 16
}} }}
> >
{`${LocationName}`} {`${locationName}`}
</Text> </Text>
) : null } ) : null}
<TouchableOpacity <TouchableOpacity
onLongPress={() => { onLongPress={() => {
return openMaps(Latitude, Longitude); return openMaps(latitude, longitude);
}} }}
> >
<Text <Text
style={[{ style={[{
color: 'black', color: 'black',
}, LocationName ? {} : {fontSize: 12, fontWeight: 600}]} }, locationName ? {} : { fontSize: 12, fontWeight: 600 }]}
> >
{`${StreetAddress}`} {`${streetAddress}`}
</Text> </Text>
<Text <Text
style={[{ style={[{
color: 'black', color: 'black',
}, LocationName ? {} : {fontSize: 12, fontWeight: 600}]} }, locationName ? {} : { fontSize: 12, fontWeight: 600 }]}
> >
{`${Town}, ${State}`} {`${town}, ${state}`}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
{AddressApartment ? ( {addressApartment ? (
<View> <View>
<View style={{ margin: 10 }} /> <View style={{ margin: 10 }} />
<Text <Text
style={[{ style={[{
color: 'black', color: 'black',
}, LocationName ? {} : {fontSize: 12, fontWeight: 600}]} }, locationName ? {} : { fontSize: 12, fontWeight: 600 }]}
> >
{`${AddressApartment}`} {`${addressApartment}`}
</Text> </Text>
</View> </View>
) : null} ) : null}
@ -361,7 +367,7 @@ export default function Call() {
justifyContent: 'center' justifyContent: 'center'
}} }}
onPress={() => { onPress={() => {
return openMaps(Latitude, Longitude); return openMaps(latitude, longitude);
}} }}
> >
<Text style={{ fontSize: 10 }}>Nav</Text> <Text style={{ fontSize: 10 }}>Nav</Text>
@ -371,7 +377,7 @@ export default function Call() {
</View> </View>
</View> </View>
</View> </View>
<View style={{ margin: 6 }}/> <View style={{ margin: 6 }} />
<View key="callCrossStreets" <View key="callCrossStreets"
style={{ style={{
padding: 2, padding: 2,
@ -400,10 +406,10 @@ export default function Call() {
textAlign: 'center' textAlign: 'center'
}} }}
> >
{Intersection1} {intersection1}
</Text> </Text>
</View> </View>
<View style={{ paddingHorizontal: "3%" }}/> <View style={{ paddingHorizontal: "2%" }} />
<View <View
style={{ style={{
borderRadius: 12, borderRadius: 12,
@ -424,11 +430,11 @@ export default function Call() {
textAlign: 'center' textAlign: 'center'
}} }}
> >
{Intersection2} {intersection2}
</Text> </Text>
</View> </View>
</View> </View>
<View style={{ margin: 6 }}/> <View style={{ margin: 6 }} />
<View key="callerInformation" style={{ padding: 2 }}> <View key="callerInformation" style={{ padding: 2 }}>
<View <View
style={{ style={{
@ -461,23 +467,23 @@ export default function Call() {
fontSize: 16 fontSize: 16
}} }}
> >
{`${Name}`} {`${name}`}
</Text> </Text>
<TouchableOpacity <TouchableOpacity
onLongPress={() => { onLongPress={() => {
return callNumber(CallBackNumber); return callNumber(callBackNumber);
}} }}
> >
<Text <Text
style={{ color: 'black', fontSize: 12 }} style={{ color: 'black', fontSize: 12 }}
> >
{`${CallBackNumber}`} {`${callBackNumber}`}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<TouchableOpacity <TouchableOpacity
onLongPress={() => { onLongPress={() => {
return callNumber(CallBackNumber); return callNumber(callBackNumber);
}} }}
> >
<Phone <Phone
@ -498,24 +504,24 @@ export default function Call() {
}} }}
/> />
</View> </View>
) : null } ) : null}
<View style={{ alignItems: 'center' }}> <View style={{ alignItems: 'center' }}>
<View style={{ flexDirection: 'row' }}> <View style={{ flexDirection: 'row' }}>
<Text style={{ fontSize: 14 }}> <Text style={{ fontSize: 14 }}>
{`${Age} `} {`${age} `}
</Text> </Text>
<Text style={{ fontSize: 14 }}> <Text style={{ fontSize: 14 }}>
{`${Gender} - `} {`${gender} - `}
</Text> </Text>
<Text style={{ fontSize: 14 }}> <Text style={{ fontSize: 14 }}>
{`Conscious: ${Conscious} | `} {`Conscious: ${conscious} | `}
</Text> </Text>
<Text style={{ fontSize: 14 }}> <Text style={{ fontSize: 14 }}>
{`Breathing: ${Breathing}`} {`Breathing: ${breathing}`}
</Text> </Text>
</View> </View>
<Text style={{ fontSize: 12 }}> <Text style={{ fontSize: 12 }}>
{`${Statement}`} {`${statement}`}
</Text> </Text>
</View> </View>
</View> </View>
@ -544,7 +550,7 @@ export default function Call() {
}} }}
> >
<View style={{ flexDirection: 'column' }}> <View style={{ flexDirection: 'column' }}>
{Units?.length > 0 ? ( {units?.length > 0 ? (
<View> <View>
{ownDepartmentResponse?.length > 0 ? ( {ownDepartmentResponse?.length > 0 ? (
<View> <View>
@ -577,7 +583,7 @@ export default function Call() {
> >
{ownDepartmentResponse?.map((unit) => { {ownDepartmentResponse?.map((unit) => {
return ( return (
<View key={unit?.Unit} <View key={unit?.unit}
style={{ style={{
flex: 1, flex: 1,
alignSelf: 'stretch', alignSelf: 'stretch',
@ -588,30 +594,31 @@ export default function Call() {
> >
<Text <Text
style={{ style={{
fontSize: 12,
fontWeight: 600, fontWeight: 600,
textAlign: 'center', textAlign: 'center',
flex: 1 flex: 1
}} }}
> >
{unit?.Unit} {unit?.unit}
</Text> </Text>
<Text style={{ textAlign: 'center', flex: 1 }}> <Text style={{ textAlign: 'center', flex: 1 }}>
{formatResponseTimes(unit?.Dispatched)} {formatResponseTimes(unit?.dispatched)}
</Text> </Text>
<Text style={{ textAlign: 'center', flex: 1 }}> <Text style={{ textAlign: 'center', flex: 1 }}>
{formatResponseTimes(unit?.Responding)} {formatResponseTimes(unit?.responding)}
</Text> </Text>
<Text style={{ textAlign: 'center', flex: 1 }}> <Text style={{ textAlign: 'center', flex: 1 }}>
{formatResponseTimes(unit?.OnScene)} {formatResponseTimes(unit?.onScene)}
</Text> </Text>
{selectedDepartment?.type === 'EMS' || {selectedDepartment?.type === 'EMS' ||
selectedDepartment?.type === 'Rescue' ? ( selectedDepartment?.type === 'Rescue' ? (
<Text style={{ textAlign: 'center', flex: 1 }}> <Text style={{ textAlign: 'center', flex: 1 }}>
{formatResponseTimes(unit?.Transporting)} {formatResponseTimes(unit?.transporting)}
</Text> </Text>
) : null } ) : null}
<Text style={{ textAlign: 'center', flex: 1 }}> <Text style={{ textAlign: 'center', flex: 1 }}>
{formatResponseTimes(unit?.InService)} {formatResponseTimes(unit?.inService)}
</Text> </Text>
</View> </View>
) )
@ -629,7 +636,7 @@ export default function Call() {
{`No ${selectedDepartment?.dept} Units Responding`} {`No ${selectedDepartment?.dept} Units Responding`}
</Text> </Text>
</View> </View>
) } )}
{mutualAidDepartmentResponse?.length > 0 ? ( {mutualAidDepartmentResponse?.length > 0 ? (
<View> <View>
<View <View
@ -661,7 +668,7 @@ export default function Call() {
/> />
{mutualAidDepartmentResponse?.map((unit) => { {mutualAidDepartmentResponse?.map((unit) => {
return ( return (
<View key={unit?.Unit} <View key={unit?.unit}
style={{ style={{
flex: 1, flex: 1,
alignSelf: 'stretch', alignSelf: 'stretch',
@ -672,36 +679,37 @@ export default function Call() {
> >
<Text <Text
style={{ style={{
fontSize: 12,
fontWeight: 600, fontWeight: 600,
textAlign: 'center', textAlign: 'center',
flex: 1 flex: 1
}} }}
> >
{unit?.Unit} {unit?.unit}
</Text> </Text>
<Text style={{ textAlign: 'center', flex: 1 }}> <Text style={{ textAlign: 'center', flex: 1 }}>
{formatResponseTimes(unit?.Dispatched)} {formatResponseTimes(unit?.dispatched)}
</Text> </Text>
<Text style={{ textAlign: 'center', flex: 1 }}> <Text style={{ textAlign: 'center', flex: 1 }}>
{formatResponseTimes(unit?.Responding)} {formatResponseTimes(unit?.responding)}
</Text> </Text>
<Text style={{ textAlign: 'center', flex: 1 }}> <Text style={{ textAlign: 'center', flex: 1 }}>
{formatResponseTimes(unit?.OnScene)} {formatResponseTimes(unit?.onScene)}
</Text> </Text>
{selectedDepartment?.type === 'EMS' || {selectedDepartment?.type === 'EMS' ||
selectedDepartment?.type === 'Rescue' ? ( selectedDepartment?.type === 'Rescue' ? (
<Text style={{ textAlign: 'center', flex: 1 }}> <Text style={{ textAlign: 'center', flex: 1 }}>
{formatResponseTimes(unit?.Transporting)} {formatResponseTimes(unit?.transporting)}
</Text> </Text>
) : null } ) : null}
<Text style={{ textAlign: 'center', flex: 1 }}> <Text style={{ textAlign: 'center', flex: 1 }}>
{formatResponseTimes(unit?.InService)} {formatResponseTimes(unit?.inService)}
</Text> </Text>
</View> </View>
) )
})} })}
</View> </View>
) : null } ) : null}
</View> </View>
) : ( ) : (
<View style={{ alignItems: 'center' }}> <View style={{ alignItems: 'center' }}>
@ -711,7 +719,7 @@ export default function Call() {
</View> </View>
</View> </View>
</View> </View>
<View style={{ margin: 6 }}/> <View style={{ margin: 6 }} />
<Text <Text
style={{ style={{
color: 'grey', color: 'grey',
@ -721,7 +729,7 @@ export default function Call() {
> >
Incident Notes Incident Notes
</Text> </Text>
<View style={{ margin: 2 }}/> <View style={{ margin: 2 }} />
<View key="incidentNotes" style={{ padding: 2 }}> <View key="incidentNotes" style={{ padding: 2 }}>
<View <View
style={{ style={{
@ -736,10 +744,10 @@ export default function Call() {
minHeight: 200 minHeight: 200
}} }}
> >
{Notes?.split('\n').map((note, index) => ( {notes?.split('\n').map((note, index) => (
<View key={index}> <View key={`notes-${index}`}>
<Text>{note}</Text> <Text>{note}</Text>
{index < Notes.split('\n').length - 1 && ( {index < notes.split('\n').length - 1 && (
<View <View
style={{ style={{
height: 1, height: 1,
@ -828,4 +836,4 @@ export default function Call() {
</DepartmentActionSheet> </DepartmentActionSheet>
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -15,6 +15,10 @@ import ActionSheet from 'react-native-actions-sheet';
const DepartmentActionSheet = styled(ActionSheet)``; const DepartmentActionSheet = styled(ActionSheet)``;
function toBase64Unicode(str) {
return btoa(new TextEncoder().encode(str).reduce((data, byte) => data + String.fromCharCode(byte), ''));
}
export default function Incidents() { export default function Incidents() {
const actionSheetRef = useRef(null); const actionSheetRef = useRef(null);
const callFeed = useCallFeed(); const callFeed = useCallFeed();
@ -49,17 +53,8 @@ export default function Incidents() {
return ( return (
<React.Fragment> <React.Fragment>
<PageHeader> <PageHeader
<View centerHeader={<TouchableOpacity
style={{
flexDirection: 'column',
height: 80,
alignItems: 'center',
justifyContent: 'flex-end',
paddingBottom: 7
}}
>
<TouchableOpacity
style={{ style={{
borderRadius: 6, borderRadius: 6,
elevation: 3, elevation: 3,
@ -84,9 +79,8 @@ export default function Incidents() {
> >
{selectedDepartment?.deptAbv} {selectedDepartment?.deptAbv}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>}
</View> />
</PageHeader>
<ScrollView> <ScrollView>
<StatusBar style="dark" /> <StatusBar style="dark" />
<SafeAreaView /> <SafeAreaView />
@ -121,7 +115,7 @@ export default function Incidents() {
router.push({ router.push({
pathname: '/call', pathname: '/call',
params: { params: {
callDetails: btoa(JSON.stringify(callItem)) callDetails: toBase64Unicode(JSON.stringify(callItem))
} }
}) })
}} }}
@ -254,7 +248,7 @@ export default function Incidents() {
</View> </View>
</View> </View>
</View> </View>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}> <View style={{ paddingTop: 5, flexDirection: 'row', justifyContent: 'space-between' }}>
<Text <Text
style={{ style={{
fontSize: 12, fontSize: 12,

View file

@ -99,14 +99,12 @@ export default function Register() {
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<StatusBar style="dark" /> <StatusBar style="dark" />
<PageHeader> <PageHeader
<View style={{ flexDirection: 'row', height: 80, alignItems: 'flex-end' }}> leftHeader={ <TouchableOpacity onPress={router.back} style={{ flexDirection: 'row', alignItems: 'center', paddingBottom: 5 }}>
<TouchableOpacity onPress={router.back} style={{ flexDirection: 'row', alignItems: 'center', paddingBottom: 5 }}>
<Ionicons name="chevron-back-outline" size={22} color="red" style={{ paddingLeft: 20 }} /> <Ionicons name="chevron-back-outline" size={22} color="red" style={{ paddingLeft: 20 }} />
<Text style={{ color: 'red', fontWeight: 600 }}>Back to Login</Text> <Text style={{ color: 'red', fontWeight: 600 }}>Back to Login</Text>
</TouchableOpacity> </TouchableOpacity>}
</View> />
</PageHeader>
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}

View file

@ -1,7 +1,8 @@
import React, { useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { View, Text, LayoutAnimation, Image, TextInput, TouchableOpacity, TouchableNativeFeedback, ScrollView } from 'react-native'; import { View, Text, LayoutAnimation, Image, TextInput, TouchableOpacity, TouchableNativeFeedback, ScrollView, Platform, Modal, Pressable } from 'react-native';
import { Picker } from '@react-native-picker/picker';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Row } from './Row';
import { Container } from './Container'; import { Container } from './Container';
export const StyledContainer = styled.View` export const StyledContainer = styled.View`
@ -204,11 +205,33 @@ const providerConversion = {
} }
export const PageHeader = ({ export const PageHeader = ({
children leftHeader = <View style={{ flex: 1 }} />,
centerHeader = <View style={{ flex: 1 }} />,
rightHeader = <View style={{ flex: 1 }} />
}) => { }) => {
return ( return (
<View style={{ position: 'sticky', top: 0, backgroundColor: '#ECEDEE', zIndex: 1, marginBottom: -100 }}> <View style={{ position: 'sticky', top: 0, backgroundColor: '#ECEDEE', zIndex: 1, marginBottom: -100 }}>
{children} <View
style={{
flexDirection: 'column',
height: 80,
alignItems: 'center',
justifyContent: 'flex-end',
paddingBottom: 7
}}
>
<View style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
paddingHorizontal: 0
}}>
{leftHeader}
{centerHeader}
{rightHeader}
</View>
</View>
</View> </View>
) )
} }
@ -228,7 +251,7 @@ export const LoginTextInput = ({
icon, icon,
isPassword = false, isPassword = false,
hidePassword = true, hidePassword = true,
setHidePassword = (boolean) => {}, setHidePassword = (boolean) => { },
...props ...props
}) => { }) => {
return ( return (
@ -239,7 +262,7 @@ export const LoginTextInput = ({
<StyledInputLabel>{label}</StyledInputLabel> <StyledInputLabel>{label}</StyledInputLabel>
<StyledTextInput {...props} /> <StyledTextInput {...props} />
{isPassword ? ( {isPassword ? (
<RightIcon onPress={() => {setHidePassword(!hidePassword)}}> <RightIcon onPress={() => { setHidePassword(!hidePassword) }}>
<Ionicons name={hidePassword ? 'eye-off-outline' : 'eye-outline'} size={30} color="gray" /> <Ionicons name={hidePassword ? 'eye-off-outline' : 'eye-outline'} size={30} color="gray" />
</RightIcon> </RightIcon>
) : null} ) : null}
@ -249,93 +272,86 @@ export const LoginTextInput = ({
export const RegisterDropdownInput = ({ export const RegisterDropdownInput = ({
label, label,
isOpen, isOpen: _isOpen,
setOpen, setOpen: _setOpen,
selectedValue, selectedValue,
onValueChange, onValueChange,
menu menu
}) => { }) => {
const [modalVisible, setModalVisible] = useState(false);
return ( return (
<Container> <Container>
<StyledInputLabel>{label}</StyledInputLabel> <StyledInputLabel>{label}</StyledInputLabel>
<TouchableOpacity activeOpacity={0.8} key={`${menu.menuName}2`} <TouchableOpacity
activeOpacity={0.8}
style={{ style={{
backgroundColor: '#E5E7EB', backgroundColor: '#E5E7EB',
marginTop: 3, marginTop: 3,
marginBottom: 10, marginBottom: 10,
borderRadius: '5px', borderRadius: 5,
flexDirection: 'row',
alignItems: 'center',
minHeight: 60,
paddingHorizontal: 15,
}} }}
onPress={() => { onPress={() => setModalVisible(true)}
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); >
LayoutAnimation.configureNext(LayoutAnimation.create(200, 'easeInEaseOut', 'opacity'));
isOpen ? setOpen(false) : setOpen(true);
}}>
<Row style={{
paddingHorizontal: 16,
paddingVertical: 16 / 1.2,
}}>
<Ionicons <Ionicons
name={menu.iconName} name={menu.iconName}
size={30} size={30}
color={menu.iconColor} color={menu.iconColor}
/> />
<Text style={{ <Text style={{ color: selectedValue ? 'black' : 'grey', fontSize: 16, paddingHorizontal: 16, flex: 1 }}>
fontSize: 16,
paddingHorizontal: 16
}}>
{selectedValue ? providerConversion[selectedValue] : menu.placeholder} {selectedValue ? providerConversion[selectedValue] : menu.placeholder}
</Text> </Text>
<DropdownArrow <DropdownArrow onPress={() => setModalVisible(!modalVisible)}>
onPress={() => { <Ionicons name={modalVisible ? "chevron-up-outline" : "chevron-down-outline"} size={30} color="gray" />
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
LayoutAnimation.configureNext(LayoutAnimation.create(200, 'easeInEaseOut', 'opacity'));
isOpen ? setOpen(false) : setOpen(true);
}}
>
<Ionicons
name={isOpen ? "chevron-up-outline" : "chevron-down-outline"}
size={30}
color="gray"
/>
</DropdownArrow> </DropdownArrow>
</Row> </TouchableOpacity>
{isOpen && <ScrollView style={{ borderRadius: '5px', backgroundColor: '#E5E7EB' }}> <Modal
{menu.dropdownList.map((subMenu, index) => { visible={modalVisible}
return ( animationType="slide"
<TouchableNativeFeedback transparent={true}
key={index} onRequestClose={() => setModalVisible(false)}
onPress={() => { >
onValueChange(subMenu.value); <Pressable
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); style={{ flex: 1 }}
LayoutAnimation.configureNext(LayoutAnimation.create(200, 'easeInEaseOut', 'opacity')); onPress={() => setModalVisible(false)}
setOpen(false); />
<View style={{
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
backgroundColor: '#fff',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
paddingBottom: 32,
paddingTop: 16,
}}>
<Pressable
style={{ flex: 1, paddingHorizontal: 15, alignItems: 'flex-end' }}
onPress={() => setModalVisible(false)}
>
<Text style={{ color: 'red', fontWeight: 600 }}>
Close
</Text>
</Pressable>
<Picker
selectedValue={selectedValue}
onValueChange={(value) => {
onValueChange(value);
}} }}
> >
<View style={{ {menu.dropdownList.map((item) => (
paddingHorizontal: 16, <Picker.Item color="black" label={item.label} value={item.value} key={item.value} />
paddingVertical: 16 / 1.5, ))}
borderTopColor: 'gray', </Picker>
borderTopWidth: .5,
marginHorizontal: 10
}}>
<Text>{subMenu.label}</Text>
{selectedValue === subMenu.value &&
<SelectedCheckmark>
<Ionicons
name="checkmark-outline"
size={30}
color="red"
/>
</SelectedCheckmark>
}
</View> </View>
</TouchableNativeFeedback> </Modal>
)
})}
</ScrollView>}
</TouchableOpacity>
</Container> </Container>
) );
} }
export const formatPhoneNumber = (e) => { export const formatPhoneNumber = (e) => {

View file

@ -20,7 +20,7 @@ export const WebSocketProvider = ({ children }) => {
} }
console.log(`🔁 Connecting (Attempt ${reconnectAttempts.current + 1}/${maxReconnectAttempts})`); console.log(`🔁 Connecting (Attempt ${reconnectAttempts.current + 1}/${maxReconnectAttempts})`);
ws.current = new WebSocket(process.env.EXPO_PUBLIC_WS_URL); ws.current = new WebSocket(`${process.env.EXPO_PUBLIC_WS_URL}/callfeed`);
ws.current.onopen = () => { ws.current.onopen = () => {
console.log('✅ WebSocket connected'); console.log('✅ WebSocket connected');

View file

@ -1,7 +1,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useDepartments } from "../useDepartments"; import { useDepartments } from "../useDepartments";
import { import {
C,
Cardiology, Cardiology,
Cpr, Cpr,
FourByFour, FourByFour,
@ -107,7 +106,7 @@ const getIncidents = async (departments, incidentStatus) => {
return response?.json() || []; return response?.json() || [];
}; };
export const useCallFeed = () => { export const useCallFeed = (callPage = false) => {
const departments = useDepartments(); const departments = useDepartments();
const { lastMessage } = useWebSocketContext(); const { lastMessage } = useWebSocketContext();
const [allCalls, setAllCalls] = useState([]); const [allCalls, setAllCalls] = useState([]);
@ -123,7 +122,7 @@ export const useCallFeed = () => {
setAllCalls(incidents); setAllCalls(incidents);
setInit(false); setInit(false);
} }
fetchData(); if (!callPage) fetchData();
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -141,7 +140,7 @@ export const useCallFeed = () => {
setAllCalls(incidents); setAllCalls(incidents);
} }
if (!init) { if (!init) {
fetchData(); if (!callPage) fetchData();
} }
}, [selectedStatus]); }, [selectedStatus]);

14
package-lock.json generated
View file

@ -12,6 +12,7 @@
"@emotion/unitless": "^0.10.0", "@emotion/unitless": "^0.10.0",
"@expo/vector-icons": "^14.0.2", "@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "^1.24.0", "@react-native-async-storage/async-storage": "^1.24.0",
"@react-native-picker/picker": "^2.11.1",
"@react-navigation/native": "^7.1.17", "@react-navigation/native": "^7.1.17",
"expo": "^53.0.20", "expo": "^53.0.20",
"expo-constants": "~17.1.7", "expo-constants": "~17.1.7",
@ -3114,6 +3115,19 @@
"react-native": "^0.0.0-0 || >=0.60 <1.0" "react-native": "^0.0.0-0 || >=0.60 <1.0"
} }
}, },
"node_modules/@react-native-picker/picker": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.1.tgz",
"integrity": "sha512-ThklnkK4fV3yynnIIRBkxxjxR4IFbdMNJVF6tlLdOJ/zEFUEFUEdXY0KmH0iYzMwY8W4/InWsLiA7AkpAbnexA==",
"license": "MIT",
"workspaces": [
"example"
],
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/@react-native/assets-registry": { "node_modules/@react-native/assets-registry": {
"version": "0.79.5", "version": "0.79.5",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.5.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.5.tgz",

View file

@ -20,6 +20,7 @@
"@emotion/unitless": "^0.10.0", "@emotion/unitless": "^0.10.0",
"@expo/vector-icons": "^14.0.2", "@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "^1.24.0", "@react-native-async-storage/async-storage": "^1.24.0",
"@react-native-picker/picker": "^2.11.1",
"@react-navigation/native": "^7.1.17", "@react-navigation/native": "^7.1.17",
"expo": "^53.0.20", "expo": "^53.0.20",
"expo-constants": "~17.1.7", "expo-constants": "~17.1.7",