Incident details encoding and UI headers
Introduces Unicode-safe base64 encoding/decoding for call details between incidents and call screens. Refactors PageHeader to accept left, center, and right header props for more flexible layouts. Updates call.jsx and register.jsx to use the new header structure, and improves department/unit rendering and modal dropdowns for department selection. Generalizes and modernizes UI code for better maintainability and cross-platform compatibility.
This commit is contained in:
parent
2f01c575db
commit
1b8533d192
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 styled from 'styled-components';
|
||||
import { router } from 'expo-router';
|
||||
import { useCallFeed } from '../hooks/useCallFeed';
|
||||
import { Platform, Linking, View, ScrollView, Text, TouchableOpacity } from 'react-native';
|
||||
import { useLocalSearchParams } from 'expo-router';
|
||||
|
|
@ -16,10 +17,14 @@ import ActionSheet from 'react-native-actions-sheet';
|
|||
|
||||
const DepartmentActionSheet = styled(ActionSheet)``;
|
||||
|
||||
function fromBase64Unicode(str) {
|
||||
return new TextDecoder().decode(Uint8Array.from(atob(str), c => c.charCodeAt(0)));
|
||||
}
|
||||
|
||||
export default function Call() {
|
||||
const { callDetails } = useLocalSearchParams();
|
||||
const actionSheetRef = useRef(null);
|
||||
const callFeed = useCallFeed();
|
||||
const callFeed = useCallFeed(true);
|
||||
|
||||
const {
|
||||
departments,
|
||||
|
|
@ -39,56 +44,56 @@ export default function Call() {
|
|||
deptList,
|
||||
} = 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 {
|
||||
IncID,
|
||||
IncNumber,
|
||||
JurisdictionNumber,
|
||||
ServiceNumber,
|
||||
ServiceID,
|
||||
IncDate,
|
||||
IncNature,
|
||||
IncNatureCode,
|
||||
IncNatureCodeDesc,
|
||||
Notes,
|
||||
Status,
|
||||
Origin,
|
||||
} = Incident;
|
||||
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;
|
||||
streetAddress,
|
||||
addressApartment,
|
||||
town,
|
||||
state,
|
||||
zipCode,
|
||||
latitude,
|
||||
longitude,
|
||||
county,
|
||||
intersection1,
|
||||
intersection2,
|
||||
locationName,
|
||||
weatherCondition,
|
||||
} = address;
|
||||
const {
|
||||
Name,
|
||||
Age,
|
||||
Gender,
|
||||
Statement,
|
||||
Conscious,
|
||||
Breathing,
|
||||
CallBackNumber,
|
||||
} = Person;
|
||||
name,
|
||||
age,
|
||||
gender,
|
||||
statement,
|
||||
conscious,
|
||||
breathing,
|
||||
callBackNumber,
|
||||
} = person;
|
||||
const {
|
||||
Units
|
||||
} = Response;
|
||||
units
|
||||
} = response;
|
||||
|
||||
const SelectedIcon = callIconMap[IncNature] || AccidentAndEmergency;
|
||||
const SelectedIcon = callIconMap[incNature] || AccidentAndEmergency;
|
||||
|
||||
const ownDepartmentResponse = Units?.map((unit) => {
|
||||
if (unit?.Department === selectedDepartment?.dept ||
|
||||
selectedDepartment?.addDepts?.includes(unit?.Department)) {
|
||||
const ownDepartmentResponse = units?.map((unit) => {
|
||||
if (unit?.department === selectedDepartment?.dept ||
|
||||
selectedDepartment?.addDepts?.includes(unit?.department)) {
|
||||
return unit;
|
||||
}
|
||||
return null;
|
||||
|
|
@ -96,9 +101,9 @@ export default function Call() {
|
|||
return filterItem;
|
||||
});
|
||||
|
||||
const mutualAidDepartmentResponse = Units?.map((unit) => {
|
||||
if (unit?.Department !== selectedDepartment?.dept &&
|
||||
!selectedDepartment?.addDepts?.includes(unit?.Department)) {
|
||||
const mutualAidDepartmentResponse = units?.map((unit) => {
|
||||
if (unit?.department !== selectedDepartment?.dept &&
|
||||
!selectedDepartment?.addDepts?.includes(unit?.department)) {
|
||||
return unit;
|
||||
}
|
||||
return null;
|
||||
|
|
@ -107,7 +112,7 @@ export default function Call() {
|
|||
});;
|
||||
|
||||
const formatResponseTimes = (time) => {
|
||||
if (time === null) {
|
||||
if (time === null || time === undefined || time === '') {
|
||||
return '';
|
||||
}
|
||||
const initTime = new Date(time);
|
||||
|
|
@ -129,16 +134,17 @@ export default function Call() {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageHeader>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
height: 80,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
paddingBottom: 7
|
||||
}}
|
||||
<PageHeader
|
||||
leftHeader={ <View style={{ flex: 1, alignItems: 'flex-start' }}>
|
||||
<TouchableOpacity
|
||||
onPress={router.back}
|
||||
style={{ flexDirection: 'row', alignItems: 'center' }}
|
||||
>
|
||||
<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
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
|
|
@ -159,15 +165,15 @@ export default function Call() {
|
|||
style={{
|
||||
color: selectedDepartmentColorPicker(selectedDepartment?.type),
|
||||
fontWeight: 600,
|
||||
fontSize: '14'
|
||||
fontSize: 14
|
||||
}}
|
||||
>
|
||||
{selectedDepartment?.deptAbv}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</PageHeader>
|
||||
<ScrollView>
|
||||
</View>}
|
||||
/>
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<StatusBar style="dark" />
|
||||
<SafeAreaView />
|
||||
<View style={{ flexDirection: 'column', padding: 20 }}>
|
||||
|
|
@ -180,8 +186,8 @@ export default function Call() {
|
|||
justifyContent: 'space-between'
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 12 }}>{formatCallDateTime(IncDate)}</Text>
|
||||
<Text style={{ fontSize: 12 }}>{formatCallTimePast(IncDate)}</Text>
|
||||
<Text style={{ fontSize: 12 }}>{formatCallDateTime(incDate)}</Text>
|
||||
<Text style={{ fontSize: 12 }}>{formatCallTimePast(incDate)}</Text>
|
||||
</View>
|
||||
<View key="callDetails" style={{ padding: 2 }}>
|
||||
<View
|
||||
|
|
@ -191,9 +197,9 @@ export default function Call() {
|
|||
backgroundColor: '#fff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowColor: callColorSelector(
|
||||
IncNatureCode,
|
||||
IncNature,
|
||||
Status
|
||||
incNatureCode,
|
||||
incNature,
|
||||
status
|
||||
),
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 5,
|
||||
|
|
@ -201,18 +207,18 @@ export default function Call() {
|
|||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', maxWidth: '90%' }}>
|
||||
<SelectedIcon
|
||||
color={callColorSelector(
|
||||
IncNatureCode,
|
||||
IncNature,
|
||||
Status
|
||||
incNatureCode,
|
||||
incNature,
|
||||
status
|
||||
)}
|
||||
opacity={0.3}
|
||||
width={56}
|
||||
height={56}
|
||||
/>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
<View style={{ flexDirection: 'column', maxWidth: '90%' }}>
|
||||
<Text
|
||||
style={{
|
||||
color: 'black',
|
||||
|
|
@ -220,28 +226,28 @@ export default function Call() {
|
|||
fontSize: 16
|
||||
}}
|
||||
>
|
||||
{`${IncNature}`}
|
||||
{`${incNature}`}
|
||||
</Text>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', maxWidth: '90%' }}>
|
||||
<Text
|
||||
style={{
|
||||
color: 'black',
|
||||
fontSize: 12,
|
||||
textShadowColor: callColorSelector(
|
||||
IncNatureCode,
|
||||
IncNature,
|
||||
Status
|
||||
incNatureCode,
|
||||
incNature,
|
||||
status
|
||||
),
|
||||
textShadowRadius: 1
|
||||
}}
|
||||
>
|
||||
{`${IncNatureCodeDesc}`}
|
||||
{`${incNatureCodeDesc}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
{Status === 'CLOSED' ? (
|
||||
{status.toLowerCase() === 'closed' ? (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
|
|
@ -258,7 +264,7 @@ export default function Call() {
|
|||
textAlign: 'right'
|
||||
}}
|
||||
>
|
||||
{`Incident #: ${ServiceNumber}`}
|
||||
{`Incident #: ${serviceNumber}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -285,8 +291,8 @@ export default function Call() {
|
|||
justifyContent: 'space-between'
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
{LocationName ? (
|
||||
<View style={{ flexDirection: 'column', maxWidth: '70%' }}>
|
||||
{locationName ? (
|
||||
<Text
|
||||
style={{
|
||||
color: 'black',
|
||||
|
|
@ -294,38 +300,38 @@ export default function Call() {
|
|||
fontSize: 16
|
||||
}}
|
||||
>
|
||||
{`${LocationName}`}
|
||||
{`${locationName}`}
|
||||
</Text>
|
||||
) : null}
|
||||
<TouchableOpacity
|
||||
onLongPress={() => {
|
||||
return openMaps(Latitude, Longitude);
|
||||
return openMaps(latitude, longitude);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[{
|
||||
color: 'black',
|
||||
}, LocationName ? {} : {fontSize: 12, fontWeight: 600}]}
|
||||
}, locationName ? {} : { fontSize: 12, fontWeight: 600 }]}
|
||||
>
|
||||
{`${StreetAddress}`}
|
||||
{`${streetAddress}`}
|
||||
</Text>
|
||||
<Text
|
||||
style={[{
|
||||
color: 'black',
|
||||
}, LocationName ? {} : {fontSize: 12, fontWeight: 600}]}
|
||||
}, locationName ? {} : { fontSize: 12, fontWeight: 600 }]}
|
||||
>
|
||||
{`${Town}, ${State}`}
|
||||
{`${town}, ${state}`}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{AddressApartment ? (
|
||||
{addressApartment ? (
|
||||
<View>
|
||||
<View style={{ margin: 10 }} />
|
||||
<Text
|
||||
style={[{
|
||||
color: 'black',
|
||||
}, LocationName ? {} : {fontSize: 12, fontWeight: 600}]}
|
||||
}, locationName ? {} : { fontSize: 12, fontWeight: 600 }]}
|
||||
>
|
||||
{`${AddressApartment}`}
|
||||
{`${addressApartment}`}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
|
@ -361,7 +367,7 @@ export default function Call() {
|
|||
justifyContent: 'center'
|
||||
}}
|
||||
onPress={() => {
|
||||
return openMaps(Latitude, Longitude);
|
||||
return openMaps(latitude, longitude);
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 10 }}>Nav</Text>
|
||||
|
|
@ -400,10 +406,10 @@ export default function Call() {
|
|||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{Intersection1}
|
||||
{intersection1}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ paddingHorizontal: "3%" }}/>
|
||||
<View style={{ paddingHorizontal: "2%" }} />
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
|
|
@ -424,7 +430,7 @@ export default function Call() {
|
|||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{Intersection2}
|
||||
{intersection2}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -461,23 +467,23 @@ export default function Call() {
|
|||
fontSize: 16
|
||||
}}
|
||||
>
|
||||
{`${Name}`}
|
||||
{`${name}`}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onLongPress={() => {
|
||||
return callNumber(CallBackNumber);
|
||||
return callNumber(callBackNumber);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{ color: 'black', fontSize: 12 }}
|
||||
>
|
||||
{`${CallBackNumber}`}
|
||||
{`${callBackNumber}`}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onLongPress={() => {
|
||||
return callNumber(CallBackNumber);
|
||||
return callNumber(callBackNumber);
|
||||
}}
|
||||
>
|
||||
<Phone
|
||||
|
|
@ -502,20 +508,20 @@ export default function Call() {
|
|||
<View style={{ alignItems: 'center' }}>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<Text style={{ fontSize: 14 }}>
|
||||
{`${Age} `}
|
||||
{`${age} `}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14 }}>
|
||||
{`${Gender} - `}
|
||||
{`${gender} - `}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14 }}>
|
||||
{`Conscious: ${Conscious} | `}
|
||||
{`Conscious: ${conscious} | `}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14 }}>
|
||||
{`Breathing: ${Breathing}`}
|
||||
{`Breathing: ${breathing}`}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={{ fontSize: 12 }}>
|
||||
{`${Statement}`}
|
||||
{`${statement}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -544,7 +550,7 @@ export default function Call() {
|
|||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
{Units?.length > 0 ? (
|
||||
{units?.length > 0 ? (
|
||||
<View>
|
||||
{ownDepartmentResponse?.length > 0 ? (
|
||||
<View>
|
||||
|
|
@ -577,7 +583,7 @@ export default function Call() {
|
|||
>
|
||||
{ownDepartmentResponse?.map((unit) => {
|
||||
return (
|
||||
<View key={unit?.Unit}
|
||||
<View key={unit?.unit}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignSelf: 'stretch',
|
||||
|
|
@ -588,30 +594,31 @@ export default function Call() {
|
|||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{unit?.Unit}
|
||||
{unit?.unit}
|
||||
</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.Dispatched)}
|
||||
{formatResponseTimes(unit?.dispatched)}
|
||||
</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.Responding)}
|
||||
{formatResponseTimes(unit?.responding)}
|
||||
</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.OnScene)}
|
||||
{formatResponseTimes(unit?.onScene)}
|
||||
</Text>
|
||||
{selectedDepartment?.type === 'EMS' ||
|
||||
selectedDepartment?.type === 'Rescue' ? (
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.Transporting)}
|
||||
{formatResponseTimes(unit?.transporting)}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.InService)}
|
||||
{formatResponseTimes(unit?.inService)}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
|
@ -661,7 +668,7 @@ export default function Call() {
|
|||
/>
|
||||
{mutualAidDepartmentResponse?.map((unit) => {
|
||||
return (
|
||||
<View key={unit?.Unit}
|
||||
<View key={unit?.unit}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignSelf: 'stretch',
|
||||
|
|
@ -672,30 +679,31 @@ export default function Call() {
|
|||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{unit?.Unit}
|
||||
{unit?.unit}
|
||||
</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.Dispatched)}
|
||||
{formatResponseTimes(unit?.dispatched)}
|
||||
</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.Responding)}
|
||||
{formatResponseTimes(unit?.responding)}
|
||||
</Text>
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.OnScene)}
|
||||
{formatResponseTimes(unit?.onScene)}
|
||||
</Text>
|
||||
{selectedDepartment?.type === 'EMS' ||
|
||||
selectedDepartment?.type === 'Rescue' ? (
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.Transporting)}
|
||||
{formatResponseTimes(unit?.transporting)}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text style={{ textAlign: 'center', flex: 1 }}>
|
||||
{formatResponseTimes(unit?.InService)}
|
||||
{formatResponseTimes(unit?.inService)}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
|
@ -736,10 +744,10 @@ export default function Call() {
|
|||
minHeight: 200
|
||||
}}
|
||||
>
|
||||
{Notes?.split('\n').map((note, index) => (
|
||||
<View key={index}>
|
||||
{notes?.split('\n').map((note, index) => (
|
||||
<View key={`notes-${index}`}>
|
||||
<Text>{note}</Text>
|
||||
{index < Notes.split('\n').length - 1 && (
|
||||
{index < notes.split('\n').length - 1 && (
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ import ActionSheet from 'react-native-actions-sheet';
|
|||
|
||||
const DepartmentActionSheet = styled(ActionSheet)``;
|
||||
|
||||
function toBase64Unicode(str) {
|
||||
return btoa(new TextEncoder().encode(str).reduce((data, byte) => data + String.fromCharCode(byte), ''));
|
||||
}
|
||||
|
||||
export default function Incidents() {
|
||||
const actionSheetRef = useRef(null);
|
||||
const callFeed = useCallFeed();
|
||||
|
|
@ -49,17 +53,8 @@ export default function Incidents() {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageHeader>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
height: 80,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
paddingBottom: 7
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
<PageHeader
|
||||
centerHeader={<TouchableOpacity
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
elevation: 3,
|
||||
|
|
@ -84,9 +79,8 @@ export default function Incidents() {
|
|||
>
|
||||
{selectedDepartment?.deptAbv}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</PageHeader>
|
||||
</TouchableOpacity>}
|
||||
/>
|
||||
<ScrollView>
|
||||
<StatusBar style="dark" />
|
||||
<SafeAreaView />
|
||||
|
|
@ -121,7 +115,7 @@ export default function Incidents() {
|
|||
router.push({
|
||||
pathname: '/call',
|
||||
params: {
|
||||
callDetails: btoa(JSON.stringify(callItem))
|
||||
callDetails: toBase64Unicode(JSON.stringify(callItem))
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
|
@ -254,7 +248,7 @@ export default function Incidents() {
|
|||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<View style={{ paddingTop: 5, flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
|
|
|
|||
|
|
@ -99,14 +99,12 @@ export default function Register() {
|
|||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<StatusBar style="dark" />
|
||||
<PageHeader>
|
||||
<View style={{ flexDirection: 'row', height: 80, alignItems: 'flex-end' }}>
|
||||
<TouchableOpacity onPress={router.back} style={{ flexDirection: 'row', alignItems: 'center', paddingBottom: 5 }}>
|
||||
<PageHeader
|
||||
leftHeader={ <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>
|
||||
</TouchableOpacity>}
|
||||
/>
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import React, { useState } from 'react';
|
||||
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 { Row } from './Row';
|
||||
import { Container } from './Container';
|
||||
|
||||
export const StyledContainer = styled.View`
|
||||
|
|
@ -204,11 +205,33 @@ const providerConversion = {
|
|||
}
|
||||
|
||||
export const PageHeader = ({
|
||||
children
|
||||
leftHeader = <View style={{ flex: 1 }} />,
|
||||
centerHeader = <View style={{ flex: 1 }} />,
|
||||
rightHeader = <View style={{ flex: 1 }} />
|
||||
}) => {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
@ -249,93 +272,86 @@ export const LoginTextInput = ({
|
|||
|
||||
export const RegisterDropdownInput = ({
|
||||
label,
|
||||
isOpen,
|
||||
setOpen,
|
||||
isOpen: _isOpen,
|
||||
setOpen: _setOpen,
|
||||
selectedValue,
|
||||
onValueChange,
|
||||
menu
|
||||
}) => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<StyledInputLabel>{label}</StyledInputLabel>
|
||||
<TouchableOpacity activeOpacity={0.8} key={`${menu.menuName}2`}
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
backgroundColor: '#E5E7EB',
|
||||
marginTop: 3,
|
||||
marginBottom: 10,
|
||||
borderRadius: '5px',
|
||||
borderRadius: 5,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
minHeight: 60,
|
||||
paddingHorizontal: 15,
|
||||
}}
|
||||
onPress={() => {
|
||||
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,
|
||||
}}>
|
||||
onPress={() => setModalVisible(true)}
|
||||
>
|
||||
<Ionicons
|
||||
name={menu.iconName}
|
||||
size={30}
|
||||
color={menu.iconColor}
|
||||
/>
|
||||
<Text style={{
|
||||
fontSize: 16,
|
||||
paddingHorizontal: 16
|
||||
}}>
|
||||
<Text style={{ color: selectedValue ? 'black' : 'grey', fontSize: 16, paddingHorizontal: 16, flex: 1 }}>
|
||||
{selectedValue ? providerConversion[selectedValue] : menu.placeholder}
|
||||
</Text>
|
||||
<DropdownArrow
|
||||
onPress={() => {
|
||||
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 onPress={() => setModalVisible(!modalVisible)}>
|
||||
<Ionicons name={modalVisible ? "chevron-up-outline" : "chevron-down-outline"} size={30} color="gray" />
|
||||
</DropdownArrow>
|
||||
</Row>
|
||||
{isOpen && <ScrollView style={{ borderRadius: '5px', backgroundColor: '#E5E7EB' }}>
|
||||
{menu.dropdownList.map((subMenu, index) => {
|
||||
return (
|
||||
<TouchableNativeFeedback
|
||||
key={index}
|
||||
onPress={() => {
|
||||
onValueChange(subMenu.value);
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
LayoutAnimation.configureNext(LayoutAnimation.create(200, 'easeInEaseOut', 'opacity'));
|
||||
setOpen(false);
|
||||
</TouchableOpacity>
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={() => setModalVisible(false)}
|
||||
>
|
||||
<Pressable
|
||||
style={{ flex: 1 }}
|
||||
onPress={() => setModalVisible(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={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16 / 1.5,
|
||||
borderTopColor: 'gray',
|
||||
borderTopWidth: .5,
|
||||
marginHorizontal: 10
|
||||
}}>
|
||||
<Text>{subMenu.label}</Text>
|
||||
{selectedValue === subMenu.value &&
|
||||
<SelectedCheckmark>
|
||||
<Ionicons
|
||||
name="checkmark-outline"
|
||||
size={30}
|
||||
color="red"
|
||||
/>
|
||||
</SelectedCheckmark>
|
||||
}
|
||||
{menu.dropdownList.map((item) => (
|
||||
<Picker.Item color="black" label={item.label} value={item.value} key={item.value} />
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
)
|
||||
})}
|
||||
</ScrollView>}
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</Container>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const formatPhoneNumber = (e) => {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export const WebSocketProvider = ({ children }) => {
|
|||
}
|
||||
|
||||
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 = () => {
|
||||
console.log('✅ WebSocket connected');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { useDepartments } from "../useDepartments";
|
||||
import {
|
||||
C,
|
||||
Cardiology,
|
||||
Cpr,
|
||||
FourByFour,
|
||||
|
|
@ -107,7 +106,7 @@ const getIncidents = async (departments, incidentStatus) => {
|
|||
return response?.json() || [];
|
||||
};
|
||||
|
||||
export const useCallFeed = () => {
|
||||
export const useCallFeed = (callPage = false) => {
|
||||
const departments = useDepartments();
|
||||
const { lastMessage } = useWebSocketContext();
|
||||
const [allCalls, setAllCalls] = useState([]);
|
||||
|
|
@ -123,7 +122,7 @@ export const useCallFeed = () => {
|
|||
setAllCalls(incidents);
|
||||
setInit(false);
|
||||
}
|
||||
fetchData();
|
||||
if (!callPage) fetchData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -141,7 +140,7 @@ export const useCallFeed = () => {
|
|||
setAllCalls(incidents);
|
||||
}
|
||||
if (!init) {
|
||||
fetchData();
|
||||
if (!callPage) fetchData();
|
||||
}
|
||||
}, [selectedStatus]);
|
||||
|
||||
|
|
|
|||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
|||
"@emotion/unitless": "^0.10.0",
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@react-native-async-storage/async-storage": "^1.24.0",
|
||||
"@react-native-picker/picker": "^2.11.1",
|
||||
"@react-navigation/native": "^7.1.17",
|
||||
"expo": "^53.0.20",
|
||||
"expo-constants": "~17.1.7",
|
||||
|
|
@ -3114,6 +3115,19 @@
|
|||
"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": {
|
||||
"version": "0.79.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.5.tgz",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"@emotion/unitless": "^0.10.0",
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@react-native-async-storage/async-storage": "^1.24.0",
|
||||
"@react-native-picker/picker": "^2.11.1",
|
||||
"@react-navigation/native": "^7.1.17",
|
||||
"expo": "^53.0.20",
|
||||
"expo-constants": "~17.1.7",
|
||||
|
|
|
|||
Loading…
Reference in a new issue