diff --git a/app/call.jsx b/app/call.jsx index ff695a4..7c98d99 100644 --- a/app/call.jsx +++ b/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,816 +17,823 @@ 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 { callDetails } = useLocalSearchParams(); + const actionSheetRef = useRef(null); + const callFeed = useCallFeed(true); - const { - departments, - callIconMap, - callColorSelector, - formatCallTimePast, - formatCallDateTime - } = callFeed; + const { + departments, + callIconMap, + callColorSelector, + formatCallTimePast, + formatCallDateTime + } = callFeed; - const { - departmentTypeMap, - accountDetails, - selectedDepartment, - setSelectedDepartment, - updateSelectedDepartment, - selectedDepartmentColorPicker, - deptList, - } = departments; + const { + departmentTypeMap, + accountDetails, + selectedDepartment, + setSelectedDepartment, + updateSelectedDepartment, + selectedDepartmentColorPicker, + deptList, + } = departments; - const decoded = atob(callDetails); + const decoded = fromBase64Unicode(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 { 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 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 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 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 formatResponseTimes = (time) => { + if (time === null || time === undefined || time === '') { + 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 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 ( - - - { + const formattedNumber = number.replace(/[()\-\s]/g, ''); + Linking.openURL(`tel:${formattedNumber}`); + }; + + return ( + + + + + Back to Incidents + + } + centerHeader={ + { + actionSheetRef.current?.show(); + }} + > + + {selectedDepartment?.deptAbv} + + + } + /> + + + + + + {formatCallDateTime(incDate)} + {formatCallTimePast(incDate)} + + + - + + + + + {`${incNature}`} + + + + {`${incNatureCodeDesc}`} + + + + + + {status.toLowerCase() === 'closed' ? ( + + This Incident is Closed. + + ) : } + + {`Incident #: ${serviceNumber}`} + + + + + + + + + + + {locationName ? ( + + {`${locationName}`} + + ) : null} + { + return openMaps(latitude, longitude); + }} + > + + {`${streetAddress}`} + + + {`${town}, ${state}`} + + + {addressApartment ? ( + + + + {`${addressApartment}`} + + + ) : null} + + + + Map + + { - actionSheetRef.current?.show(); + return openMaps(latitude, longitude); }} - > - - {selectedDepartment?.deptAbv} - - + > + Nav + + + + - - - - - - - {formatCallDateTime(IncDate)} - {formatCallTimePast(IncDate)} - - - - - - - - - {`${IncNature}`} - - - - {`${IncNatureCodeDesc}`} - - - - - - {Status === 'CLOSED' ? ( - - This Incident is Closed. - - ) : } - - {`Incident #: ${ServiceNumber}`} - - - - - - - - - - - {LocationName ? ( - - {`${LocationName}`} - - ) : null } - { - return openMaps(Latitude, Longitude); - }} - > - - {`${StreetAddress}`} - - - {`${Town}, ${State}`} - - - {AddressApartment ? ( - - - - {`${AddressApartment}`} - - - ) : null} - - - - Map - - { - return openMaps(Latitude, Longitude); - }} - > - Nav - - - - - - - - - - - {Intersection1} - - - - - - {Intersection2} - - - - - - - {selectedDepartment?.supervisor ? ( - - - - - - {`${Name}`} - - { - return callNumber(CallBackNumber); - }} - > - - {`${CallBackNumber}`} - - - - { - return callNumber(CallBackNumber); - }} - > - - - - - - - ) : null } - - - - {`${Age} `} - - - {`${Gender} - `} - - - {`Conscious: ${Conscious} | `} - - - {`Breathing: ${Breathing}`} - - - - {`${Statement}`} - - - - - - - - - - - {Units?.length > 0 ? ( - - {ownDepartmentResponse?.length > 0 ? ( - - - - Units - Disp - Resp - Arr - {selectedDepartment?.type === 'EMS' || selectedDepartment?.type === 'Rescue' ? ( - Trans - ) : null} - BIS - - - - {ownDepartmentResponse?.map((unit) => { - return ( - - - {unit?.Unit} - - - {formatResponseTimes(unit?.Dispatched)} - - - {formatResponseTimes(unit?.Responding)} - - - {formatResponseTimes(unit?.OnScene)} - - {selectedDepartment?.type === 'EMS' || - selectedDepartment?.type === 'Rescue' ? ( - - {formatResponseTimes(unit?.Transporting)} - - ) : null } - - {formatResponseTimes(unit?.InService)} - - - ) - })} - - - - ) : ( - - - {`No ${selectedDepartment?.dept} Units Responding`} - - - ) } - {mutualAidDepartmentResponse?.length > 0 ? ( - - - - M/A - Disp - Resp - Arr - {selectedDepartment?.type === 'EMS' || selectedDepartment?.type === 'Rescue' ? ( - Trans - ) : null} - BIS - - - {mutualAidDepartmentResponse?.map((unit) => { - return ( - - - {unit?.Unit} - - - {formatResponseTimes(unit?.Dispatched)} - - - {formatResponseTimes(unit?.Responding)} - - - {formatResponseTimes(unit?.OnScene)} - - {selectedDepartment?.type === 'EMS' || - selectedDepartment?.type === 'Rescue' ? ( - - {formatResponseTimes(unit?.Transporting)} - - ) : null } - - {formatResponseTimes(unit?.InService)} - - - ) - })} - - ) : null } - - ) : ( - - No Units Responding - - )} - - - - - - Incident Notes - - - - - {Notes?.split('\n').map((note, index) => ( - - {note} - {index < Notes.split('\n').length - 1 && ( - - )} - - ))} - - - - - - - - + + + - - {deptList?.map((item) => { - return ( - - { - actionSheetRef.current?.hide(); - return updateSelectedDepartment( - selectedDepartment?.deptId, - item?.deptId - ) - }} - > - - - {item?.dept} - - {item?.primary ? : null} - - {`${item?.deptAbv} - ${departmentTypeMap[item?.type]}`} - - - ); - })} + + {intersection1} + + + + + + {intersection2} + + + + + + + {selectedDepartment?.supervisor ? ( + + + + + + {`${name}`} + + { + return callNumber(callBackNumber); + }} + > + + {`${callBackNumber}`} + + + + { + return callNumber(callBackNumber); + }} + > + + + + + - - - ); - } \ No newline at end of file + ) : null} + + + + {`${age} `} + + + {`${gender} - `} + + + {`Conscious: ${conscious} | `} + + + {`Breathing: ${breathing}`} + + + + {`${statement}`} + + + + + + + + + + + {units?.length > 0 ? ( + + {ownDepartmentResponse?.length > 0 ? ( + + + + Units + Disp + Resp + Arr + {selectedDepartment?.type === 'EMS' || selectedDepartment?.type === 'Rescue' ? ( + Trans + ) : null} + BIS + + + + {ownDepartmentResponse?.map((unit) => { + return ( + + + {unit?.unit} + + + {formatResponseTimes(unit?.dispatched)} + + + {formatResponseTimes(unit?.responding)} + + + {formatResponseTimes(unit?.onScene)} + + {selectedDepartment?.type === 'EMS' || + selectedDepartment?.type === 'Rescue' ? ( + + {formatResponseTimes(unit?.transporting)} + + ) : null} + + {formatResponseTimes(unit?.inService)} + + + ) + })} + + + + ) : ( + + + {`No ${selectedDepartment?.dept} Units Responding`} + + + )} + {mutualAidDepartmentResponse?.length > 0 ? ( + + + + M/A + Disp + Resp + Arr + {selectedDepartment?.type === 'EMS' || selectedDepartment?.type === 'Rescue' ? ( + Trans + ) : null} + BIS + + + {mutualAidDepartmentResponse?.map((unit) => { + return ( + + + {unit?.unit} + + + {formatResponseTimes(unit?.dispatched)} + + + {formatResponseTimes(unit?.responding)} + + + {formatResponseTimes(unit?.onScene)} + + {selectedDepartment?.type === 'EMS' || + selectedDepartment?.type === 'Rescue' ? ( + + {formatResponseTimes(unit?.transporting)} + + ) : null} + + {formatResponseTimes(unit?.inService)} + + + ) + })} + + ) : null} + + ) : ( + + No Units Responding + + )} + + + + + + Incident Notes + + + + + {notes?.split('\n').map((note, index) => ( + + {note} + {index < notes.split('\n').length - 1 && ( + + )} + + ))} + + + + + + + + + + {deptList?.map((item) => { + return ( + + { + actionSheetRef.current?.hide(); + return updateSelectedDepartment( + selectedDepartment?.deptId, + item?.deptId + ) + }} + > + + + {item?.dept} + + {item?.primary ? : null} + + {`${item?.deptAbv} - ${departmentTypeMap[item?.type]}`} + + + ); + })} + + + + ); +} \ No newline at end of file diff --git a/app/incidents.jsx b/app/incidents.jsx index ac6737c..a5c0308 100644 --- a/app/incidents.jsx +++ b/app/incidents.jsx @@ -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 ( - - - {selectedDepartment?.deptAbv} - - - + } + /> @@ -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() { - + - - - + Back to Login - - - + } + /> , + centerHeader = , + rightHeader = }) => { return ( - {children} + + + {leftHeader} + {centerHeader} + {rightHeader} + + ) } export const PageFooter = ({ - children + children }) => { return ( @@ -224,11 +247,11 @@ export const PageFooter = ({ } export const LoginTextInput = ({ - label, - icon, - isPassword = false, - hidePassword = true, - setHidePassword = (boolean) => {}, + label, + icon, + isPassword = false, + hidePassword = true, + setHidePassword = (boolean) => { }, ...props }) => { return ( @@ -239,7 +262,7 @@ export const LoginTextInput = ({ {label} {isPassword ? ( - {setHidePassword(!hidePassword)}}> + { setHidePassword(!hidePassword) }}> ) : null} @@ -249,115 +272,108 @@ export const LoginTextInput = ({ export const RegisterDropdownInput = ({ label, - isOpen, - setOpen, + isOpen: _isOpen, + setOpen: _setOpen, selectedValue, onValueChange, menu }) => { + const [modalVisible, setModalVisible] = useState(false); + return ( {label} - { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - LayoutAnimation.configureNext(LayoutAnimation.create(200, 'easeInEaseOut', 'opacity')); - isOpen ? setOpen(false) : setOpen(true); + onPress={() => setModalVisible(true)} + > + + + {selectedValue ? providerConversion[selectedValue] : menu.placeholder} + + setModalVisible(!modalVisible)}> + + + + setModalVisible(false)} + > + setModalVisible(false)} + /> + - - - - {selectedValue ? providerConversion[selectedValue] : menu.placeholder} - - { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - LayoutAnimation.configureNext(LayoutAnimation.create(200, 'easeInEaseOut', 'opacity')); - isOpen ? setOpen(false) : setOpen(true); + setModalVisible(false)} + > + + Close + + + { + onValueChange(value); }} > - - - - {isOpen && - {menu.dropdownList.map((subMenu, index) => { - return ( - { - onValueChange(subMenu.value); - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - LayoutAnimation.configureNext(LayoutAnimation.create(200, 'easeInEaseOut', 'opacity')); - setOpen(false); - }} - > - - {subMenu.label} - {selectedValue === subMenu.value && - - - - } - - - ) - })} - } - + {menu.dropdownList.map((item) => ( + + ))} + + + - ) + ); } -export const formatPhoneNumber = (e) => { +export const formatPhoneNumber = (e) => { let formattedNumber; const length = e?.length; const regex = () => e.replace(/[^0-9\.]+/g, ""); const areaCode = () => `(${regex().slice(0, 3)})`; const firstSix = () => `${areaCode()} ${regex().slice(3, 6)}`; - const trailer = (start) => `${regex().slice(start, regex().length)}`; + const trailer = (start) => `${regex().slice(start, regex().length)}`; if (length <= 3) { formattedNumber = regex(); } else if (length === 4) { formattedNumber = `${areaCode()} ${trailer(3)}`; } else if (length === 5) { - formattedNumber = `${areaCode().replace(")", "")}`; + formattedNumber = `${areaCode().replace(")", "")}`; } else if (length >= 5 && length <= 9) { - formattedNumber = `${areaCode()} ${trailer(3)}`; + formattedNumber = `${areaCode()} ${trailer(3)}`; } else if (length >= 10) { formattedNumber = `${firstSix()}-${trailer(6)}`; - } + } return formattedNumber; }; \ No newline at end of file diff --git a/contexts/WebSocketContext.js b/contexts/WebSocketContext.js index 641a59e..f9354ca 100644 --- a/contexts/WebSocketContext.js +++ b/contexts/WebSocketContext.js @@ -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'); diff --git a/hooks/useCallFeed/useCallFeed.jsx b/hooks/useCallFeed/useCallFeed.jsx index 9437be1..31f413a 100644 --- a/hooks/useCallFeed/useCallFeed.jsx +++ b/hooks/useCallFeed/useCallFeed.jsx @@ -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]); diff --git a/package-lock.json b/package-lock.json index 79022e2..5329613 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 38e0e53..b3739a8 100644 --- a/package.json +++ b/package.json @@ -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",