Feature/notification system #26
8 changed files with 939 additions and 909 deletions
248
app/call.jsx
248
app/call.jsx
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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'}
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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
14
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue