Merge pull request #18 from Doble-Technologies/feature/callpage
Feature/callpage
This commit is contained in:
commit
87257ac859
19 changed files with 6503 additions and 8660 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
.env
|
||||
node_modules/
|
||||
.expo/
|
||||
dist/
|
||||
|
|
|
|||
8
app.json
8
app.json
|
|
@ -23,13 +23,9 @@
|
|||
},
|
||||
"package": "com.anonymous.testapplication"
|
||||
},
|
||||
// "web": {
|
||||
// "bundler": "metro",
|
||||
// "output": "static",
|
||||
// "favicon": "./assets/images/favicon.png"
|
||||
// },
|
||||
"plugins": [
|
||||
"expo-router"
|
||||
"expo-router",
|
||||
"expo-font"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
|
|
|
|||
|
|
@ -1,40 +1,26 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { router, Stack } from 'expo-router';
|
||||
import { WebSocketProvider } from '../contexts/WebSocketContext';
|
||||
|
||||
export const unstable_settings = {
|
||||
// Ensure any route can link back to `/`
|
||||
initialRouteName: 'login',
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
|
||||
const [auth, setAuth] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (auth) {
|
||||
router.replace('/explore');
|
||||
} else {
|
||||
router.replace('/login');
|
||||
}
|
||||
router.replace('/login');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (auth) {
|
||||
router.replace('/explore');
|
||||
} else {
|
||||
router.replace('/login');
|
||||
}
|
||||
}, [auth]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="login" />
|
||||
<Stack.Screen name="register" />
|
||||
<Stack.Screen name="explore" />
|
||||
</Stack>
|
||||
<WebSocketProvider>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="login" />
|
||||
</Stack>
|
||||
</WebSocketProvider>
|
||||
);
|
||||
}
|
||||
831
app/call.jsx
Normal file
831
app/call.jsx
Normal file
|
|
@ -0,0 +1,831 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useCallFeed } from '../hooks/useCallFeed';
|
||||
import { Platform, Linking, View, ScrollView, Text, TouchableOpacity } from 'react-native';
|
||||
import { useLocalSearchParams, router } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { AccidentAndEmergency } from "healthicons-react-native/dist/outline";
|
||||
import { Phone } from "healthicons-react-native/dist/filled";
|
||||
import {
|
||||
PageHeader,
|
||||
PageFooter,
|
||||
} from '../components/generalHelpers.jsx';
|
||||
import ActionSheet from 'react-native-actions-sheet';
|
||||
|
||||
const DepartmentActionSheet = styled(ActionSheet)``;
|
||||
|
||||
export default function Call() {
|
||||
const { callDetails } = useLocalSearchParams();
|
||||
const actionSheetRef = useRef(null);
|
||||
const callFeed = useCallFeed();
|
||||
|
||||
const {
|
||||
departments,
|
||||
callIconMap,
|
||||
callColorSelector,
|
||||
formatCallTimePast,
|
||||
formatCallDateTime
|
||||
} = callFeed;
|
||||
|
||||
const {
|
||||
departmentTypeMap,
|
||||
accountDetails,
|
||||
selectedDepartment,
|
||||
setSelectedDepartment,
|
||||
updateSelectedDepartment,
|
||||
selectedDepartmentColorPicker,
|
||||
deptList,
|
||||
} = departments;
|
||||
|
||||
const decoded = atob(callDetails);
|
||||
|
||||
const { Incident, Address, Person, Response } = JSON.parse(decoded);
|
||||
const { CallThemes } = accountDetails;
|
||||
const {
|
||||
IncID,
|
||||
IncNumber,
|
||||
JurisdictionNumber,
|
||||
ServiceNumber,
|
||||
ServiceID,
|
||||
IncDate,
|
||||
IncNature,
|
||||
IncNatureCode,
|
||||
IncNatureCodeDesc,
|
||||
Notes,
|
||||
Status,
|
||||
Origin,
|
||||
} = Incident;
|
||||
const {
|
||||
StreetAddress,
|
||||
AddressApartment,
|
||||
Town,
|
||||
State,
|
||||
ZipCode,
|
||||
Latitude,
|
||||
Longitude,
|
||||
County,
|
||||
Intersection1,
|
||||
Intersection2,
|
||||
LocationName,
|
||||
WeatherCondition,
|
||||
} = Address;
|
||||
const {
|
||||
Name,
|
||||
Age,
|
||||
Gender,
|
||||
Statement,
|
||||
Conscious,
|
||||
Breathing,
|
||||
CallBackNumber,
|
||||
} = Person;
|
||||
const {
|
||||
Units
|
||||
} = Response;
|
||||
|
||||
const SelectedIcon = callIconMap[IncNature] || AccidentAndEmergency;
|
||||
|
||||
const ownDepartmentResponse = Units?.map((unit) => {
|
||||
if (unit?.Department === selectedDepartment?.dept ||
|
||||
selectedDepartment?.addDepts?.includes(unit?.Department)) {
|
||||
return unit;
|
||||
}
|
||||
return null;
|
||||
})?.filter((filterItem) => {
|
||||
return filterItem;
|
||||
});
|
||||
|
||||
const mutualAidDepartmentResponse = Units?.map((unit) => {
|
||||
if (unit?.Department !== selectedDepartment?.dept &&
|
||||
!selectedDepartment?.addDepts?.includes(unit?.Department)) {
|
||||
return unit;
|
||||
}
|
||||
return null;
|
||||
})?.filter((filterItem) => {
|
||||
return filterItem;
|
||||
});;
|
||||
|
||||
const formatResponseTimes = (time) => {
|
||||
if (time === null) {
|
||||
return '';
|
||||
}
|
||||
const initTime = new Date(time);
|
||||
const hours = initTime.getHours().toString().padStart(2, '0');
|
||||
const minutes = initTime.getMinutes().toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
const openMaps = (latitude, longitude) => {
|
||||
const daddr = `${latitude},${longitude}`;
|
||||
const company = Platform.OS === "ios" ? "apple" : "google";
|
||||
Linking.openURL(`http://maps.${company}.com/maps?daddr=${daddr}`);
|
||||
};
|
||||
|
||||
const callNumber = (number) => {
|
||||
const formattedNumber = number.replace(/[()\-\s]/g, '');
|
||||
Linking.openURL(`tel:${formattedNumber}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageHeader>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
height: 80,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
paddingBottom: 7
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
elevation: 3,
|
||||
backgroundColor: '#fff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: '#333',
|
||||
shadowOpacity: .8,
|
||||
shadowRadius: 2,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 2,
|
||||
}}
|
||||
onPress={() => {
|
||||
actionSheetRef.current?.show();
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: selectedDepartmentColorPicker(selectedDepartment?.type),
|
||||
fontWeight: 600,
|
||||
fontSize: '14'
|
||||
}}
|
||||
>
|
||||
{selectedDepartment?.deptAbv}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</PageHeader>
|
||||
<ScrollView>
|
||||
<StatusBar style="dark" />
|
||||
<SafeAreaView />
|
||||
<View style={{ flexDirection: 'column', padding: 20 }}>
|
||||
<View key="callDateAndTime"
|
||||
style={{
|
||||
paddingBottom: 6,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 12 }}>{formatCallDateTime(IncDate)}</Text>
|
||||
<Text style={{ fontSize: 12 }}>{formatCallTimePast(IncDate)}</Text>
|
||||
</View>
|
||||
<View key="callDetails" style={{ padding: 2 }}>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
elevation: 3,
|
||||
backgroundColor: '#fff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: callColorSelector(
|
||||
IncNatureCode,
|
||||
IncNature,
|
||||
Status
|
||||
),
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 5,
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<SelectedIcon
|
||||
color={callColorSelector(
|
||||
IncNatureCode,
|
||||
IncNature,
|
||||
Status
|
||||
)}
|
||||
opacity={0.3}
|
||||
width={56}
|
||||
height={56}
|
||||
/>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
<Text
|
||||
style={{
|
||||
color: 'black',
|
||||
fontWeight: 600,
|
||||
fontSize: 16
|
||||
}}
|
||||
>
|
||||
{`${IncNature}`}
|
||||
</Text>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<Text
|
||||
style={{
|
||||
color: 'black',
|
||||
fontSize: 12,
|
||||
textShadowColor: callColorSelector(
|
||||
IncNatureCode,
|
||||
IncNature,
|
||||
Status
|
||||
),
|
||||
textShadowRadius: 1
|
||||
}}
|
||||
>
|
||||
{`${IncNatureCodeDesc}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
{Status === 'CLOSED' ? (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: '#'
|
||||
}}
|
||||
>
|
||||
This Incident is Closed.
|
||||
</Text>
|
||||
) : <Text></Text> }
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
textAlign: 'right'
|
||||
}}
|
||||
>
|
||||
{`Incident #: ${ServiceNumber}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ margin: 6 }}/>
|
||||
<View key="callLocation" style={{ padding: 2 }}>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
elevation: 3,
|
||||
backgroundColor: '#fff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: 'grey',
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 1,
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'top',
|
||||
justifyContent: 'space-between'
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
{LocationName ? (
|
||||
<Text
|
||||
style={{
|
||||
color: 'black',
|
||||
fontWeight: 600,
|
||||
fontSize: 16
|
||||
}}
|
||||
>
|
||||
{`${LocationName}`}
|
||||
</Text>
|
||||
) : null }
|
||||
<TouchableOpacity
|
||||
onLongPress={() => {
|
||||
return openMaps(Latitude, Longitude);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[{
|
||||
color: 'black',
|
||||
}, LocationName ? {} : {fontSize: 12, fontWeight: 600}]}
|
||||
>
|
||||
{`${StreetAddress}`}
|
||||
</Text>
|
||||
<Text
|
||||
style={[{
|
||||
color: 'black',
|
||||
}, LocationName ? {} : {fontSize: 12, fontWeight: 600}]}
|
||||
>
|
||||
{`${Town}, ${State}`}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{AddressApartment ? (
|
||||
<View>
|
||||
<View style={{ margin: 10 }} />
|
||||
<Text
|
||||
style={[{
|
||||
color: 'black',
|
||||
}, LocationName ? {} : {fontSize: 12, fontWeight: 600}]}
|
||||
>
|
||||
{`${AddressApartment}`}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
elevation: 3,
|
||||
backgroundColor: '#fff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: 'grey',
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 1,
|
||||
padding: 25,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 12 }}>Map</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
elevation: 3,
|
||||
backgroundColor: '#ECEDEE',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: 'grey',
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 1,
|
||||
marginTop: 6,
|
||||
paddingLeft: 6,
|
||||
paddingVertical: 6,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
onPress={() => {
|
||||
return openMaps(Latitude, Longitude);
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 10 }}>Nav</Text>
|
||||
<Ionicons name="chevron-forward-outline" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ margin: 6 }}/>
|
||||
<View key="callCrossStreets"
|
||||
style={{
|
||||
padding: 2,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-evenly',
|
||||
marginHorizontal: 2,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
elevation: 3,
|
||||
backgroundColor: '#fff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: 'grey',
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 1,
|
||||
paddingVertical: 14,
|
||||
width: "48%"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{Intersection1}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ paddingHorizontal: "3%" }}/>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
elevation: 3,
|
||||
backgroundColor: '#fff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: 'grey',
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 1,
|
||||
paddingVertical: 14,
|
||||
width: "48%"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{Intersection2}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ margin: 6 }}/>
|
||||
<View key="callerInformation" style={{ padding: 2 }}>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
elevation: 3,
|
||||
backgroundColor: '#fff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: 'grey',
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 1,
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
{selectedDepartment?.supervisor ? (
|
||||
<View>
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginHorizontal: 10
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
<Text
|
||||
style={{
|
||||
color: 'black',
|
||||
fontWeight: 600,
|
||||
fontSize: 16
|
||||
}}
|
||||
>
|
||||
{`${Name}`}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onLongPress={() => {
|
||||
return callNumber(CallBackNumber);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{ color: 'black', fontSize: 12 }}
|
||||
>
|
||||
{`${CallBackNumber}`}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onLongPress={() => {
|
||||
return callNumber(CallBackNumber);
|
||||
}}
|
||||
>
|
||||
<Phone
|
||||
color='blue'
|
||||
opacity={0.3}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
marginVertical: 5,
|
||||
height: 1,
|
||||
width: '100%',
|
||||
backgroundColor: 'grey'
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null }
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<Text style={{ fontSize: 14 }}>
|
||||
{`${Age} `}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14 }}>
|
||||
{`${Gender} - `}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14 }}>
|
||||
{`Concious: ${Conscious} | `}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14 }}>
|
||||
{`Breathing: ${Breathing}`}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={{ fontSize: 12 }}>
|
||||
{`${Statement}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<View
|
||||
style={{
|
||||
marginVertical: 10,
|
||||
height: 2,
|
||||
width: '95%',
|
||||
backgroundColor: 'grey'
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View key="respondingUnitInformation" style={{ padding: 2 }}>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
elevation: 3,
|
||||
backgroundColor: '#fff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: 'grey',
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 1,
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
{Units?.length > 0 ? (
|
||||
<View>
|
||||
{ownDepartmentResponse?.length > 0 ? (
|
||||
<View>
|
||||
<View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-evenly', alignItems: 'center' }}>
|
||||
<Text style={{ fontWeight: '600', textAlign: 'center', flex: 1 }}>Units</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>Disp</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>Resp</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>Arr</Text>
|
||||
{selectedDepartment?.type === 'EMS' || selectedDepartment?.type === 'Rescue' ? (
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>Trans</Text>
|
||||
) : null}
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>BIS</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
marginVertical: 5,
|
||||
height: 1,
|
||||
width: '95%',
|
||||
backgroundColor: 'grey',
|
||||
alignSelf: 'center'
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{ownDepartmentResponse?.map((unit) => {
|
||||
return (
|
||||
<View key={unit?.Unit}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-evenly',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{unit?.Unit}
|
||||
</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.Dispatched)}
|
||||
</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.Responding)}
|
||||
</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.OnScene)}
|
||||
</Text>
|
||||
{selectedDepartment?.type === 'EMS' ||
|
||||
selectedDepartment?.type === 'Rescue' ? (
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.Transporting)}
|
||||
</Text>
|
||||
) : null }
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.InService)}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text
|
||||
style={{
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{`No ${selectedDepartment?.dept} Units Responding`}
|
||||
</Text>
|
||||
</View>
|
||||
) }
|
||||
{mutualAidDepartmentResponse?.length > 0 ? (
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
marginVertical: 5,
|
||||
height: 2,
|
||||
width: '100%',
|
||||
backgroundColor: 'grey',
|
||||
}}
|
||||
/>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-evenly', alignItems: 'center' }}>
|
||||
<Text style={{ fontWeight: '600', textAlign: 'center', flex: 1 }}>M/A</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>Disp</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>Resp</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>Arr</Text>
|
||||
{selectedDepartment?.type === 'EMS' || selectedDepartment?.type === 'Rescue' ? (
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>Trans</Text>
|
||||
) : null}
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>BIS</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
marginVertical: 5,
|
||||
height: 1,
|
||||
width: '95%',
|
||||
backgroundColor: 'grey',
|
||||
alignSelf: 'center'
|
||||
}}
|
||||
/>
|
||||
{mutualAidDepartmentResponse?.map((unit) => {
|
||||
return (
|
||||
<View key={unit?.Unit}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-evenly',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{unit?.Unit}
|
||||
</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.Dispatched)}
|
||||
</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.Responding)}
|
||||
</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.OnScene)}
|
||||
</Text>
|
||||
{selectedDepartment?.type === 'EMS' ||
|
||||
selectedDepartment?.type === 'Rescue' ? (
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.Transporting)}
|
||||
</Text>
|
||||
) : null }
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.InService)}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
) : null }
|
||||
</View>
|
||||
) : (
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text>No Units Responding</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ margin: 6 }}/>
|
||||
<Text
|
||||
style={{
|
||||
color: 'grey',
|
||||
fontWeight: 600,
|
||||
paddingLeft: 10
|
||||
}}
|
||||
>
|
||||
Incident Notes
|
||||
</Text>
|
||||
<View style={{ margin: 2 }}/>
|
||||
<View key="incidentNotes" style={{ padding: 2 }}>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
elevation: 3,
|
||||
backgroundColor: '#fff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: 'grey',
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 1,
|
||||
padding: 10,
|
||||
minHeight: 200
|
||||
}}
|
||||
>
|
||||
{Notes?.split('\n').map((note, index) => (
|
||||
<View key={index}>
|
||||
<Text>{note}</Text>
|
||||
{index < Notes.split('\n').length - 1 && (
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
backgroundColor: 'grey',
|
||||
marginVertical: 10
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<PageFooter>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
height: 100,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
paddingTop: 7
|
||||
}}
|
||||
/>
|
||||
</PageFooter>
|
||||
<DepartmentActionSheet
|
||||
ref={actionSheetRef}
|
||||
gestureEnabled
|
||||
containerStyle={{
|
||||
height: "50%",
|
||||
width: "100%",
|
||||
backgroundColor: '#ECEDEE',
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'column', padding: 20 }}>
|
||||
{deptList?.map((item) => {
|
||||
return (
|
||||
<View style={{ padding: 2 }} key={item?.deptId}>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
elevation: 3,
|
||||
backgroundColor: item?.selected ? 'grey' : '#fff',
|
||||
shadowOffset: { width: 1, height: 1 },
|
||||
shadowColor: '#333',
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 2,
|
||||
marginHorizontal: 20,
|
||||
marginVertical: 6,
|
||||
padding: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
actionSheetRef.current?.hide();
|
||||
return updateSelectedDepartment(
|
||||
selectedDepartment?.deptId,
|
||||
item?.deptId
|
||||
)
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<Text
|
||||
style={{
|
||||
color: selectedDepartmentColorPicker(
|
||||
item?.type
|
||||
),
|
||||
fontWeight: 600,
|
||||
fontSize: '16'
|
||||
}}
|
||||
>
|
||||
{item?.dept}
|
||||
</Text>
|
||||
{item?.primary ? <Ionicons name="star" size={16} color="yellow" style={{
|
||||
paddingLeft: 20,
|
||||
shadowColor: '#333',
|
||||
shadowOffset: 1,
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 6
|
||||
}} /> : null}
|
||||
</View>
|
||||
<Text>{`${item?.deptAbv} - ${departmentTypeMap[item?.type]}`}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</DepartmentActionSheet>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
358
app/incidents.jsx
Normal file
358
app/incidents.jsx
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useCallFeed } from '../hooks/useCallFeed';
|
||||
import { router } from 'expo-router';
|
||||
import { Platform, Linking, View, ScrollView, Text, TouchableOpacity } from 'react-native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import {
|
||||
PageHeader,
|
||||
PageFooter,
|
||||
} from '../components/generalHelpers.jsx';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { AccidentAndEmergency } from "healthicons-react-native/dist/outline";
|
||||
import ActionSheet from 'react-native-actions-sheet';
|
||||
|
||||
const DepartmentActionSheet = styled(ActionSheet)``;
|
||||
|
||||
export default function Incidents() {
|
||||
const actionSheetRef = useRef(null);
|
||||
const callFeed = useCallFeed();
|
||||
|
||||
const {
|
||||
departments,
|
||||
callIconMap,
|
||||
callDetails,
|
||||
callColorSelector,
|
||||
formatCallTimePast,
|
||||
formatCallDateTime
|
||||
} = callFeed;
|
||||
|
||||
const sortedAndFilteredCalls = callDetails
|
||||
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
||||
.filter((item, index, self) => {
|
||||
return index === self.findIndex(i => {
|
||||
return `${i?.data?.Incident?.IncID}${i?.data?.Response?.ServiceName}` === `${item?.data?.Incident?.IncID}${item?.data?.Response?.ServiceName}`
|
||||
});
|
||||
});
|
||||
|
||||
const {
|
||||
departmentTypeMap,
|
||||
accountDetails,
|
||||
deptList,
|
||||
setDeptList,
|
||||
selectedDepartment,
|
||||
setSelectedDepartment,
|
||||
updateSelectedDepartment,
|
||||
selectedDepartmentColorPicker,
|
||||
} = departments;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageHeader>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
height: 80,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
paddingBottom: 7
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
elevation: 3,
|
||||
backgroundColor: '#fff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: '#333',
|
||||
shadowOpacity: .8,
|
||||
shadowRadius: 2,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 2,
|
||||
}}
|
||||
onPress={() => {
|
||||
actionSheetRef.current?.show();
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: selectedDepartmentColorPicker(selectedDepartment?.type),
|
||||
fontWeight: 600,
|
||||
fontSize: 14
|
||||
}}
|
||||
>
|
||||
{selectedDepartment?.deptAbv}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</PageHeader>
|
||||
<ScrollView>
|
||||
<StatusBar style="dark" />
|
||||
<SafeAreaView />
|
||||
<View style={{ flexDirection: 'column', padding: 20 }}>
|
||||
{sortedAndFilteredCalls?.length ? (
|
||||
sortedAndFilteredCalls?.map((callItem, index) => {
|
||||
const { data: call, timestamp } = callItem;
|
||||
const { Incident, Address, Response } = call;
|
||||
const {
|
||||
ServiceNumber,
|
||||
IncDate,
|
||||
IncNature,
|
||||
IncNatureCode,
|
||||
IncNatureCodeDesc,
|
||||
Status,
|
||||
} = Incident;
|
||||
const {
|
||||
StreetAddress,
|
||||
AddressApartment,
|
||||
Town,
|
||||
State,
|
||||
LocationName,
|
||||
} = Address;
|
||||
const {
|
||||
ServiceName
|
||||
} = Response;
|
||||
const SelectedIcon = callIconMap[IncNature] || AccidentAndEmergency;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={`callDetails - ${timestamp}`}
|
||||
style={{ padding: 2 }}
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: '/landing',
|
||||
params: {
|
||||
callDetails: btoa(JSON.stringify(call))
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
elevation: 3,
|
||||
backgroundColor: '#fff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: callColorSelector(
|
||||
IncNatureCode,
|
||||
IncNature,
|
||||
Status
|
||||
),
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 5,
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
<View key="callDateAndTime"
|
||||
style={{
|
||||
paddingBottom: 6,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 12 }}>{formatCallDateTime(IncDate)}</Text>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ fontSize: 12 }}>{formatCallTimePast(IncDate)}</Text>
|
||||
{Status === 'CLOSED' ? (
|
||||
<Ionicons
|
||||
name="lock-closed-outline"
|
||||
color='red'
|
||||
style={{
|
||||
shadowColor: 'black',
|
||||
shadowOffset: 0,
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 10
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<SelectedIcon
|
||||
color={callColorSelector(
|
||||
IncNatureCode,
|
||||
IncNature,
|
||||
Status
|
||||
)}
|
||||
opacity={0.3}
|
||||
width={56}
|
||||
height={56}
|
||||
/>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
{LocationName ? (
|
||||
<Text
|
||||
style={{
|
||||
color: 'black',
|
||||
fontWeight: 600,
|
||||
fontSize: 16
|
||||
}}
|
||||
>
|
||||
{`${LocationName}`}
|
||||
</Text>
|
||||
) : (
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<Text
|
||||
style={[{
|
||||
color: 'black',
|
||||
fontSize: 12,
|
||||
fontWeight: 600
|
||||
}]}
|
||||
>
|
||||
{`${StreetAddress}`}
|
||||
</Text>
|
||||
{AddressApartment ? (
|
||||
<Text
|
||||
style={[{
|
||||
color: 'black',
|
||||
fontSize: 12,
|
||||
fontWeight: 600
|
||||
}]}
|
||||
>
|
||||
{` - ${AddressApartment}`}
|
||||
</Text>
|
||||
) : null }
|
||||
<Text
|
||||
style={[{
|
||||
color: 'black',
|
||||
fontSize: 12,
|
||||
fontWeight: 600
|
||||
}]}
|
||||
>
|
||||
{` ${Town}, ${State}`}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text
|
||||
style={{
|
||||
color: 'black',
|
||||
fontWeight: 600,
|
||||
fontSize: 16
|
||||
}}
|
||||
>
|
||||
{`${IncNature}`}
|
||||
</Text>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<Text
|
||||
style={{
|
||||
color: 'black',
|
||||
fontSize: 12,
|
||||
textShadowColor: callColorSelector(
|
||||
IncNatureCode,
|
||||
IncNature,
|
||||
Status
|
||||
),
|
||||
textShadowRadius: 1
|
||||
}}
|
||||
>
|
||||
{`${IncNatureCodeDesc}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ padding: 0 }}>
|
||||
<View>
|
||||
<Ionicons name="chevron-forward-outline" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
Service: {ServiceName}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
textAlign: 'right'
|
||||
}}
|
||||
>
|
||||
{`Incident #: ${ServiceNumber}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
})) : (
|
||||
<Text>There are no Calls</Text>
|
||||
) }
|
||||
</View>
|
||||
</ScrollView>
|
||||
<PageFooter>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
height: 100,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
paddingTop: 7
|
||||
}}
|
||||
/>
|
||||
</PageFooter>
|
||||
<DepartmentActionSheet
|
||||
ref={actionSheetRef}
|
||||
gestureEnabled
|
||||
containerStyle={{
|
||||
height: "50%",
|
||||
width: "100%",
|
||||
backgroundColor: '#ECEDEE',
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'column', padding: 20 }}>
|
||||
{deptList?.map((item) => {
|
||||
return (
|
||||
<View style={{ padding: 2 }} key={item?.deptId}>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
elevation: 3,
|
||||
backgroundColor: item?.selected ? 'grey' : '#fff',
|
||||
shadowOffset: { width: 1, height: 1 },
|
||||
shadowColor: '#333',
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 2,
|
||||
marginHorizontal: 20,
|
||||
marginVertical: 6,
|
||||
padding: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
actionSheetRef.current?.hide();
|
||||
return updateSelectedDepartment(
|
||||
selectedDepartment?.deptId,
|
||||
item?.deptId
|
||||
)
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<Text
|
||||
style={{
|
||||
color: selectedDepartmentColorPicker(
|
||||
item?.type
|
||||
),
|
||||
fontWeight: 600,
|
||||
fontSize: 16
|
||||
}}
|
||||
>
|
||||
{item?.dept}
|
||||
</Text>
|
||||
{item?.primary ? <Ionicons name="star" size={16} color="yellow" style={{
|
||||
paddingLeft: 20,
|
||||
shadowColor: '#333',
|
||||
shadowOffset: 1,
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 6
|
||||
}} /> : null}
|
||||
</View>
|
||||
<Text>{`${item?.deptAbv} - ${departmentTypeMap[item?.type]}`}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</DepartmentActionSheet>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useFormik } from 'formik';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Link } from 'expo-router';
|
||||
import { router, Link } from 'expo-router';
|
||||
import {
|
||||
PageHeader,
|
||||
StyledContainer,
|
||||
|
|
@ -19,11 +20,12 @@ import {
|
|||
ExtraText,
|
||||
TextLinkContent,
|
||||
LoginTextInput
|
||||
} from './generalHelpers.jsx';
|
||||
} from '../components/generalHelpers.jsx';
|
||||
|
||||
export default function TabLayout() {
|
||||
export default function Login() {
|
||||
const [hidePassword, setHidePassword] = useState(true);
|
||||
const [loginButtonDisabled, setLoginButtonDisabled] = useState(true);
|
||||
const [auth, setAuth] = useState(false);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
|
|
@ -33,6 +35,7 @@ export default function TabLayout() {
|
|||
onSubmit: (values) => {
|
||||
values.number = values.number.replace(/[()\-\s]/g, '');
|
||||
console.log(values);
|
||||
setAuth(true);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -48,9 +51,23 @@ export default function TabLayout() {
|
|||
}
|
||||
}, [formValues])
|
||||
|
||||
useEffect(() => {
|
||||
// Temp for Testing
|
||||
// if (!auth) {
|
||||
// setTimeout(() => {
|
||||
// router.navigate('./incidents');
|
||||
// }, 1000);
|
||||
// }
|
||||
if (auth) {
|
||||
router.navigate('./incidents');
|
||||
}
|
||||
}, [auth])
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageHeader />
|
||||
<PageHeader>
|
||||
<View style={{ flexDirection: 'row', height: 80, alignItems: 'center' }} />
|
||||
</PageHeader>
|
||||
<StyledContainer>
|
||||
<StatusBar style="dark" />
|
||||
<SafeAreaView />
|
||||
|
|
@ -95,6 +112,17 @@ export default function TabLayout() {
|
|||
</Link>
|
||||
</ExtraView>
|
||||
</StyledFormArea>
|
||||
<Line />
|
||||
<Text>Temporary Area:</Text>
|
||||
<View>
|
||||
<Link href='./incidents'>
|
||||
<TextLinkContent>Incidents</TextLinkContent>
|
||||
</Link>
|
||||
<Link href='./landing'>
|
||||
<TextLinkContent>Landing</TextLinkContent>
|
||||
</Link>
|
||||
</View>
|
||||
<Line />
|
||||
</InnerContainer>
|
||||
</StyledContainer>
|
||||
</React.Fragment>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
MessageBox,
|
||||
LoginTextInput,
|
||||
RegisterDropdownInput,
|
||||
} from './generalHelpers.jsx';
|
||||
} from '../components/generalHelpers.jsx';
|
||||
|
||||
export default function Register() {
|
||||
|
||||
|
|
@ -65,10 +65,12 @@ export default function Register() {
|
|||
return (
|
||||
<View>
|
||||
<PageHeader>
|
||||
<TouchableOpacity onPress={router.back} style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Ionicons name="arrow-back-outline" size={30} color="red" style={{ paddingLeft: 20 }} />
|
||||
<Text style={{ color: 'red', fontWeight: 600 }}>Back to Login</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={{ flexDirection: 'row', height: 80, alignItems: 'flex-end' }}>
|
||||
<TouchableOpacity onPress={router.back} style={{ flexDirection: 'row', alignItems: 'center', paddingBottom: 5 }}>
|
||||
<Ionicons name="chevron-back-outline" size={22} color="red" style={{ paddingLeft: 20 }} />
|
||||
<Text style={{ color: 'red', fontWeight: 600 }}>Back to Login</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</PageHeader>
|
||||
<ScrollView>
|
||||
<StyledContainer>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import styled from 'styled-components';
|
||||
import { View, Text, LayoutAnimation, Image, TextInput, TouchableOpacity, TouchableNativeFeedback, ScrollView } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Row } from '../components/Row';
|
||||
import { Container } from '../components/Container';
|
||||
import { Row } from './Row';
|
||||
import { Container } from './Container';
|
||||
|
||||
export const StyledContainer = styled.View`
|
||||
flex: 1;
|
||||
|
|
@ -207,10 +207,18 @@ export const PageHeader = ({
|
|||
children
|
||||
}) => {
|
||||
return (
|
||||
<View style={{ position: 'sticky', top: 0, backgroundColor: '#ECEDEE', zIndex: 1, marginBottom: -80 }}>
|
||||
<View style={{ flexDirection: 'row', height: 80, alignItems: 'flex-end' }}>
|
||||
{children}
|
||||
</View>
|
||||
<View style={{ position: 'sticky', top: 0, backgroundColor: '#ECEDEE', zIndex: 1, marginBottom: -100 }}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export const PageFooter = ({
|
||||
children
|
||||
}) => {
|
||||
return (
|
||||
<View style={{ position: 'fixed', top: 0, backgroundColor: '#ECEDEE', zIndex: 1 }}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
78
contexts/WebSocketContext.js
Normal file
78
contexts/WebSocketContext.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import React, { createContext, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export const WebSocketContext = createContext(null);
|
||||
|
||||
export const WebSocketProvider = ({ children }) => {
|
||||
const ws = useRef(null);
|
||||
const reconnectInterval = useRef(null);
|
||||
const reconnectAttempts = useRef(0);
|
||||
const maxReconnectAttempts = 5;
|
||||
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [lastMessage, setLastMessage] = useState(null);
|
||||
|
||||
const connect = () => {
|
||||
if (reconnectAttempts.current >= maxReconnectAttempts) {
|
||||
console.warn('❌ Max reconnect attempts reached. Giving up.');
|
||||
clearInterval(reconnectInterval.current);
|
||||
ws.current?.close();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`🔁 Connecting (Attempt ${reconnectAttempts.current + 1}/${maxReconnectAttempts})`);
|
||||
ws.current = new WebSocket(process.env.EXPO_PUBLIC_WS_URL);
|
||||
|
||||
ws.current.onopen = () => {
|
||||
console.log('✅ WebSocket connected');
|
||||
setIsConnected(true);
|
||||
reconnectAttempts.current = 0;
|
||||
if (reconnectInterval.current) {
|
||||
clearInterval(reconnectInterval.current);
|
||||
reconnectInterval.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
ws.current.onmessage = (e) => {
|
||||
console.log('📩 Message received:', e.data);
|
||||
setLastMessage(e.data);
|
||||
};
|
||||
|
||||
ws.current.onerror = (e) => {
|
||||
console.error('❌ WebSocket error:', e.message);
|
||||
};
|
||||
|
||||
ws.current.onclose = (e) => {
|
||||
console.log('🔌 WebSocket closed:', e.code, e.reason);
|
||||
setIsConnected(false);
|
||||
if (!reconnectInterval.current && reconnectAttempts.current < maxReconnectAttempts) {
|
||||
reconnectInterval.current = setInterval(() => {
|
||||
reconnectAttempts.current += 1;
|
||||
connect();
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
clearInterval(reconnectInterval.current);
|
||||
ws.current?.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sendMessage = (msg) => {
|
||||
if (ws.current?.readyState === WebSocket.OPEN) {
|
||||
ws.current.send(JSON.stringify(msg));
|
||||
} else {
|
||||
console.warn('⚠️ WebSocket is not open. Message not sent.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<WebSocketContext.Provider value={{ sendMessage, lastMessage, isConnected }}>
|
||||
{children}
|
||||
</WebSocketContext.Provider>
|
||||
);
|
||||
};
|
||||
2
hooks/index.js
Normal file
2
hooks/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { useCallFeed } from './useCallFeed';
|
||||
export { useDepartments } from './useDepartments';
|
||||
1
hooks/useCallFeed/index.js
Normal file
1
hooks/useCallFeed/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { useCallFeed } from "./useCallFeed";
|
||||
236
hooks/useCallFeed/useCallFeed.jsx
Normal file
236
hooks/useCallFeed/useCallFeed.jsx
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useDepartments } from '../useDepartments';
|
||||
import { C, Cardiology, Cpr, FourByFour } from "healthicons-react-native/dist/outline";
|
||||
import { useWebSocketContext } from '../useWebSocketContext';
|
||||
|
||||
const callIconMap = {
|
||||
"CHEST PAIN|HEART PROBLEMS": Cardiology,
|
||||
"CARDIAC ARREST|DEATH": Cpr,
|
||||
"MOTOR VEHICLE COLLISION (MVC)": FourByFour,
|
||||
}
|
||||
|
||||
// Squares
|
||||
|
||||
// Cariology - Heart
|
||||
// BurnUnit - Fire
|
||||
// AccidentAndEmergency - Misc.
|
||||
// Rheumatology - Bone/Crash
|
||||
// Sonography - Baby
|
||||
// PainManagement - CPR/Cardiac Arrest
|
||||
// Respiratory - Diff Breathing
|
||||
|
||||
// Others
|
||||
|
||||
// HeartOrgan - Heart
|
||||
// Burn - Burns
|
||||
// FHIR - Structure Fire
|
||||
// Sonogram - Baby
|
||||
// SUV - Crash
|
||||
// Joints - Bone
|
||||
// Pain - CPR/Cardiac Arrest
|
||||
// Skull - CPR/Cardiac Arrest
|
||||
// CPR - CPR/Cardiac Arrest
|
||||
// Pneumonia - Diff Breathing
|
||||
// CoughingAlt - Diff Breathing
|
||||
// Diabetes - Diabetic Emergency
|
||||
// BloodDrop - Bleeding Emergencies
|
||||
// Bacteria - Sick
|
||||
// RuralClinic - Medical Facility Response
|
||||
|
||||
const callDetails = {
|
||||
"Incident": {
|
||||
"IncID": 75,
|
||||
"IncNumber": 6873,
|
||||
"JurisdictionNumber": 3,
|
||||
"ServiceNumber": 42,
|
||||
"ServiceID": 45,
|
||||
"IncDate": "2024-09-25T01:01:01.55",
|
||||
// "IncNature": "CHEST PAIN|HEART PROBLEMS",
|
||||
"IncNature": "MOTOR VEHICLE COLLISION (MVC)",
|
||||
// "IncNature": "CARDIAC ARREST|DEATH",
|
||||
"IncNatureCode": "ALS",
|
||||
"IncNatureCodeDesc": "ALS PRIORITY (ALS)",
|
||||
"Notes": "570, 16:30> 311 Responding\n580, 16:25> Call Dispatched",
|
||||
"Status": "OPEN",
|
||||
"Origin": "911"
|
||||
},
|
||||
"Address": {
|
||||
"StreetAddress": "275 E Main St",
|
||||
"AddressApartment": "IFO",
|
||||
"Town": "Bridgeport",
|
||||
"State": "CT",
|
||||
"ZipCode": "06608",
|
||||
"Latitude": 41.178435683035445,
|
||||
"Longitude": -73.18194442701176,
|
||||
"County": "Fairfield",
|
||||
"Intersection1": "E MAIN ST",
|
||||
"Intersection2": "STRATFORD AVE",
|
||||
"LocationName": "Chipotle Mexican Grill",
|
||||
"WeatherCondition": "Foggy"
|
||||
},
|
||||
"Person": {
|
||||
"Name": "John Doe",
|
||||
"Age": 19,
|
||||
"Gender": "Female",
|
||||
"Statement": "BLOOD PRESSURE 56/41 - IN AND OUT OF CON",
|
||||
"Conscious": "No",
|
||||
"Breathing": "Yes",
|
||||
"CallBackNumber": "(223) 456-7890"
|
||||
},
|
||||
"Response": {
|
||||
"IncID": 75,
|
||||
"ResponseID": 75,
|
||||
"ServiceID": 45,
|
||||
"ServiceName": "DARIEN EMS",
|
||||
"Units": [
|
||||
{
|
||||
"Unit": 311,
|
||||
"Department": 'Darien EMS',
|
||||
"Dispatched": "2024-09-25T01:01:01.55",
|
||||
"Responding": "2024-09-25T01:02:02.55",
|
||||
"OnScene": "2024-09-25T01:10:10.55",
|
||||
"Transporting": "2024-09-25T01:25:01.55",
|
||||
"InService": "2024-09-25T02:00:01.55",
|
||||
},
|
||||
{
|
||||
"Unit": 315,
|
||||
"Department": 'Darien EMS Supv',
|
||||
"Dispatched": "2024-09-25T01:01:01.55",
|
||||
"Responding": "2024-09-25T01:03:03.15",
|
||||
"OnScene": "2024-09-25T01:11:11.55",
|
||||
"Transporting": null,
|
||||
"InService": "2024-09-25T02:10:01.55",
|
||||
},
|
||||
{
|
||||
"Unit": 310,
|
||||
"Department": 'Darien EMS',
|
||||
"Dispatched": "2024-09-25T01:01:01.55",
|
||||
"Responding": "2024-09-25T01:01:01.55",
|
||||
"OnScene": "2024-09-25T01:06:06.55",
|
||||
"Transporting": "2024-09-25T01:25:01.55",
|
||||
"InService": "2024-09-25T02:15:01.55",
|
||||
},
|
||||
{
|
||||
"Unit": 'NHT20',
|
||||
"Department": 'Noroton Heights Fire Department',
|
||||
"Dispatched": "2024-09-25T01:01:01.55",
|
||||
"Responding": "2024-09-25T01:08:08.55",
|
||||
"OnScene": "2024-09-25T01:12:12.55",
|
||||
"Transporting": null,
|
||||
"InService": "2024-09-25T01:01:01.55",
|
||||
},
|
||||
{
|
||||
"Unit": 'NFDE31',
|
||||
"Department": 'Noroton Fire Department',
|
||||
"Dispatched": "2024-09-25T01:01:01.55",
|
||||
"Responding": "2024-09-25T01:07:07.55",
|
||||
"OnScene": "2024-09-25T01:14:14.55",
|
||||
"Transporting": null,
|
||||
"InService": "2024-09-25T01:01:01.55",
|
||||
},
|
||||
{
|
||||
"Unit": 'DFDT43',
|
||||
"Department": 'Darien Fire Department',
|
||||
"Dispatched": "2024-09-25T01:01:01.55",
|
||||
"Responding": "2024-09-25T01:10:10.55",
|
||||
"OnScene": "2024-09-25T01:18:18.55",
|
||||
"Transporting": null,
|
||||
"InService": "2024-09-25T01:01:01.55",
|
||||
},
|
||||
{
|
||||
"Unit": 1514,
|
||||
"Department": 'Greenwich EMS',
|
||||
"Dispatched": "2024-09-25T01:15:15.55",
|
||||
"Responding": "2024-09-25T01:30:30.55",
|
||||
"OnScene": "2024-09-25T01:45:45.55",
|
||||
"Transporting": "2024-09-25T02:05:15.55",
|
||||
"InService": "2024-09-25T02:25:01.55",
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const formatCallTimePast = (callValue) => {
|
||||
const initDate = new Date(callValue);
|
||||
const currentTime = new Date();
|
||||
|
||||
const timeDifference = currentTime - initDate;
|
||||
|
||||
const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((timeDifference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((timeDifference % (1000 * 60)) / 1000);
|
||||
if (days && days !== 0) {
|
||||
return `${days} day${days === 1 ? '' : 's'} ago`;
|
||||
} else if (hours && hours !== 0) {
|
||||
return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
||||
} else if (minutes && minutes !== 0) {
|
||||
return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
|
||||
} else if (seconds && seconds !== 0) {
|
||||
return `${seconds} second${seconds === 1 ? '' : 's'} ago`;
|
||||
}
|
||||
return `Unknown Time Past`;
|
||||
}
|
||||
|
||||
const formatCallDateTime = (callValue) => {
|
||||
const initDate = new Date(callValue);
|
||||
if (initDate) {
|
||||
const formattedDate = `${initDate.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}`;
|
||||
const hours = initDate.getHours().toString().padStart(2, '0');
|
||||
const minutes = initDate.getMinutes().toString().padStart(2, '0');
|
||||
const formattedTime = `${hours}:${minutes}`;
|
||||
|
||||
return `${formattedDate} - ${formattedTime}`;
|
||||
}
|
||||
return 'Date Unavailable';
|
||||
}
|
||||
|
||||
export const useCallFeed = () => {
|
||||
const departments = useDepartments();
|
||||
const { lastMessage } = useWebSocketContext();
|
||||
const [allCalls, setAllCalls] = useState([]);
|
||||
const { CallThemes } = departments?.accountDetails;
|
||||
const {
|
||||
CriticalCallList,
|
||||
HighCallList,
|
||||
MediumCallList,
|
||||
LowCallList,
|
||||
} = CallThemes;
|
||||
|
||||
useEffect(() => {
|
||||
if (lastMessage) {
|
||||
const parsedMessage = JSON?.parse(lastMessage);
|
||||
if (parsedMessage?.data) {
|
||||
setAllCalls([...allCalls, parsedMessage]);
|
||||
}
|
||||
}
|
||||
}, [lastMessage]);
|
||||
|
||||
const callColorSelector = (callAcuity, cardiacArrestCall, status) => {
|
||||
if (status === 'CLOSED') {
|
||||
return '#0000CD';
|
||||
} else if (CriticalCallList.includes(cardiacArrestCall)) {
|
||||
return '#8B0000';
|
||||
} else if (HighCallList.includes(callAcuity)) {
|
||||
return "#FF0000";
|
||||
} else if (MediumCallList.includes(callAcuity)) {
|
||||
return "#FF8C00";
|
||||
} else if (LowCallList.includes(callAcuity)) {
|
||||
return "#228B22";
|
||||
}
|
||||
return 'grey';
|
||||
};
|
||||
|
||||
return {
|
||||
departments,
|
||||
callIconMap,
|
||||
callDetails: allCalls,
|
||||
callColorSelector,
|
||||
formatCallTimePast,
|
||||
formatCallDateTime
|
||||
}
|
||||
}
|
||||
1
hooks/useDepartments/index.js
Normal file
1
hooks/useDepartments/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { useDepartments } from './useDepartments';
|
||||
122
hooks/useDepartments/useDepartments.jsx
Normal file
122
hooks/useDepartments/useDepartments.jsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
|
||||
const departmentTypeMap = {
|
||||
EMS: 'Medical Services',
|
||||
Fire: 'Fire Department',
|
||||
Rescue: 'Fire & EMS'
|
||||
}
|
||||
|
||||
const accountDetails = {
|
||||
"CallThemes" : {
|
||||
"CriticalCallList": [
|
||||
"CARDIAC ARREST|DEATH",
|
||||
],
|
||||
"HighCallList": [
|
||||
"ALS"
|
||||
],
|
||||
"MediumCallList": [
|
||||
"ALS-STANDARD",
|
||||
"BLS-PRIORITY"
|
||||
],
|
||||
"LowCallList": [
|
||||
"BLS-STANDARD"
|
||||
]
|
||||
},
|
||||
"InitList": [
|
||||
{
|
||||
deptId: 0,
|
||||
dept: 'Darien EMS',
|
||||
addDepts: [
|
||||
'Darien EMS Supv'
|
||||
],
|
||||
deptAbv: 'DEMS',
|
||||
rank: 'Assistant Director',
|
||||
rankAbv: 'Asst. Director',
|
||||
type: 'EMS',
|
||||
primary: true,
|
||||
selected: true,
|
||||
supervisor: true,
|
||||
admin: true,
|
||||
hasVolunteer: true,
|
||||
},
|
||||
{
|
||||
deptId: 1,
|
||||
dept: 'Noroton Fire Department',
|
||||
deptAbv: 'NFD',
|
||||
rank: 'Lieutenant',
|
||||
rankAbv: 'Lt.',
|
||||
type: 'Fire',
|
||||
primary: false,
|
||||
selected: false,
|
||||
supervisor: true,
|
||||
admin: true,
|
||||
hasVolunteer: true,
|
||||
},
|
||||
{
|
||||
deptId: 2,
|
||||
dept: 'Stamford Fire Department',
|
||||
deptAbv: 'SFD',
|
||||
rank: 'Paramedic',
|
||||
rankAbv: 'EMT-P',
|
||||
type: 'Rescue',
|
||||
primary: false,
|
||||
selected: false,
|
||||
supervisor: false,
|
||||
admin: true,
|
||||
hasVolunteer: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const useDepartments = () => {
|
||||
const { InitList } = accountDetails;
|
||||
const [deptList, setDeptList] = useState(InitList);
|
||||
const [selectedDepartment, setSelectedDepartment] = useState(deptList?.find((dept) => {
|
||||
return dept?.primary;
|
||||
}));
|
||||
|
||||
const updateSelectedDepartment = (currentSelectedId, newSelectedId) => {
|
||||
if (currentSelectedId !== newSelectedId) {
|
||||
setDeptList(deptList?.map((item) => {
|
||||
if (item?.selected) {
|
||||
item.selected = false;
|
||||
}
|
||||
if (item?.deptId === newSelectedId) {
|
||||
item.selected = true;
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
const selectedDepartmentColorPicker = (deptartmentType) => {
|
||||
if (deptartmentType === 'Fire') {
|
||||
return '#FF0000';
|
||||
} else if (deptartmentType === 'EMS') {
|
||||
return '#FF8C00';
|
||||
} else if (deptartmentType === 'Rescue') {
|
||||
return '#0000CD';
|
||||
}
|
||||
return 'grey';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (deptList) {
|
||||
setSelectedDepartment(deptList?.find((dept) => {
|
||||
return dept?.selected;
|
||||
}));
|
||||
}
|
||||
}, [deptList]);
|
||||
|
||||
return {
|
||||
departmentTypeMap,
|
||||
accountDetails,
|
||||
deptList,
|
||||
setDeptList,
|
||||
selectedDepartment,
|
||||
setSelectedDepartment,
|
||||
updateSelectedDepartment,
|
||||
selectedDepartmentColorPicker
|
||||
}
|
||||
}
|
||||
1
hooks/useWebSocketContext/index.js
Normal file
1
hooks/useWebSocketContext/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { useWebSocketContext } from './useWebSocketContext';
|
||||
4
hooks/useWebSocketContext/useWebSocketContext.js
Normal file
4
hooks/useWebSocketContext/useWebSocketContext.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { useContext } from 'react';
|
||||
import { WebSocketContext } from '../../contexts/WebSocketContext';
|
||||
|
||||
export const useWebSocketContext = () => useContext(WebSocketContext);
|
||||
13365
package-lock.json
generated
13365
package-lock.json
generated
File diff suppressed because it is too large
Load diff
43
package.json
43
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "tones",
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
|
|
@ -17,35 +17,38 @@
|
|||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@react-navigation/native": "^6.0.2",
|
||||
"expo": "~51.0.24",
|
||||
"expo-constants": "~16.0.2",
|
||||
"expo-font": "~12.0.9",
|
||||
"expo-linking": "~6.3.1",
|
||||
"expo-router": "~3.5.20",
|
||||
"expo-splash-screen": "~0.27.5",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"expo-system-ui": "~3.0.7",
|
||||
"expo-web-browser": "~13.0.3",
|
||||
"expo": "^52.0.46",
|
||||
"expo-constants": "~17.0.8",
|
||||
"expo-font": "~13.0.4",
|
||||
"expo-linking": "~7.0.5",
|
||||
"expo-router": "~4.0.20",
|
||||
"expo-splash-screen": "~0.29.24",
|
||||
"expo-status-bar": "~2.0.1",
|
||||
"expo-system-ui": "~4.0.9",
|
||||
"expo-web-browser": "~14.0.2",
|
||||
"formik": "^2.4.6",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.74.3",
|
||||
"healthicons-react-native": "^3.0.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-actions-sheet": "^0.9.7",
|
||||
"react-native-dropdown-picker": "^5.4.6",
|
||||
"react-native-gesture-handler": "~2.16.1",
|
||||
"react-native-reanimated": "~3.10.1",
|
||||
"react-native-safe-area-context": "4.10.5",
|
||||
"react-native-screens": "3.31.1",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-reanimated": "~3.16.1",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-svg": "15.8.0",
|
||||
"react-native-textinput-effects": "^0.6.3",
|
||||
"react-native-web": "~0.19.10",
|
||||
"react-native-web": "~0.19.13",
|
||||
"styled-components": "^6.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/react": "~18.2.45",
|
||||
"@types/react": "~18.3.12",
|
||||
"@types/react-test-renderer": "^18.0.7",
|
||||
"jest": "^29.2.1",
|
||||
"jest-expo": "~51.0.3",
|
||||
"jest-expo": "~52.0.6",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"typescript": "~5.3.3"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,5 +14,5 @@
|
|||
"**/*.jsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts"
|
||||
]
|
||||
, "contexts/WebSocketContext.js" ]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue