Feature/notification system #26
8 changed files with 939 additions and 909 deletions
1588
app/call.jsx
1588
app/call.jsx
File diff suppressed because it is too large
Load diff
|
|
@ -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`
|
||||
|
|
@ -44,7 +45,7 @@ export const StyledTextInput = styled.TextInput`
|
|||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
export const StyledInputLabel = styled.Text`
|
||||
export const StyledInputLabel = styled.Text`
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
`;
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -228,7 +251,7 @@ export const LoginTextInput = ({
|
|||
icon,
|
||||
isPassword = false,
|
||||
hidePassword = true,
|
||||
setHidePassword = (boolean) => {},
|
||||
setHidePassword = (boolean) => { },
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
|
|
@ -239,7 +262,7 @@ export const LoginTextInput = ({
|
|||
<StyledInputLabel>{label}</StyledInputLabel>
|
||||
<StyledTextInput {...props} />
|
||||
{isPassword ? (
|
||||
<RightIcon onPress={() => {setHidePassword(!hidePassword)}}>
|
||||
<RightIcon onPress={() => { setHidePassword(!hidePassword) }}>
|
||||
<Ionicons name={hidePassword ? 'eye-off-outline' : 'eye-outline'} size={30} color="gray" />
|
||||
</RightIcon>
|
||||
) : null}
|
||||
|
|
@ -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);
|
||||
onPress={() => setModalVisible(true)}
|
||||
>
|
||||
<Ionicons
|
||||
name={menu.iconName}
|
||||
size={30}
|
||||
color={menu.iconColor}
|
||||
/>
|
||||
<Text style={{ color: selectedValue ? 'black' : 'grey', fontSize: 16, paddingHorizontal: 16, flex: 1 }}>
|
||||
{selectedValue ? providerConversion[selectedValue] : menu.placeholder}
|
||||
</Text>
|
||||
<DropdownArrow onPress={() => setModalVisible(!modalVisible)}>
|
||||
<Ionicons name={modalVisible ? "chevron-up-outline" : "chevron-down-outline"} size={30} color="gray" />
|
||||
</DropdownArrow>
|
||||
</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,
|
||||
}}>
|
||||
<Row style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16 / 1.2,
|
||||
}}>
|
||||
<Ionicons
|
||||
name={menu.iconName}
|
||||
size={30}
|
||||
color={menu.iconColor}
|
||||
/>
|
||||
<Text style={{
|
||||
fontSize: 16,
|
||||
paddingHorizontal: 16
|
||||
}}>
|
||||
{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);
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={isOpen ? "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);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
}
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
)
|
||||
})}
|
||||
</ScrollView>}
|
||||
</TouchableOpacity>
|
||||
{menu.dropdownList.map((item) => (
|
||||
<Picker.Item color="black" label={item.label} value={item.value} key={item.value} />
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
</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