Feature/notification system #26

Open
mattdimegs wants to merge 34 commits from feature/notification-system into main
8 changed files with 939 additions and 909 deletions
Showing only changes of commit 1b8533d192 - Show all commits

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,
@ -258,7 +264,7 @@ export default function Call() {
textAlign: 'right' textAlign: 'right'
}} }}
> >
{`Incident #: ${ServiceNumber}`} {`Incident #: ${serviceNumber}`}
</Text> </Text>
</View> </View>
</View> </View>
@ -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>
@ -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,7 +430,7 @@ export default function Call() {
textAlign: 'center' textAlign: 'center'
}} }}
> >
{Intersection2} {intersection2}
</Text> </Text>
</View> </View>
</View> </View>
@ -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
@ -502,20 +508,20 @@ export default function Call() {
<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>
) )
@ -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,30 +679,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>
) )
@ -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,

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>
) )
} }
@ -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",