diff --git a/frontend/occupi-mobile4/app/_layout.tsx b/frontend/occupi-mobile4/app/_layout.tsx index 5decefd6..0ebbed75 100644 --- a/frontend/occupi-mobile4/app/_layout.tsx +++ b/frontend/occupi-mobile4/app/_layout.tsx @@ -6,6 +6,7 @@ import { Stack } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; import 'react-native-reanimated'; import { GluestackUIProvider } from "@gluestack-ui/themed"; +import { ThemeProvider } from '@/components/ThemeContext'; import { NavBarProvider } from '@/components/NavBarProvider'; import { config } from "@gluestack-ui/config"; // Optional if you want to use default theme @@ -32,36 +33,38 @@ export default function RootLayout() { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } \ No newline at end of file diff --git a/frontend/occupi-mobile4/components/NavBar.tsx b/frontend/occupi-mobile4/components/NavBar.tsx index ddeef052..f67ac580 100644 --- a/frontend/occupi-mobile4/components/NavBar.tsx +++ b/frontend/occupi-mobile4/components/NavBar.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { StyleSheet } from 'react-native'; import { Text, Button, Icon, CalendarDaysIcon, BellIcon } from '@gluestack-ui/themed'; import { Feather } from '@expo/vector-icons'; @@ -6,25 +6,38 @@ import { FontAwesome6, Ionicons } from '@expo/vector-icons'; import { router } from 'expo-router'; import { BlurView } from 'expo-blur'; import { useColorScheme } from 'react-native'; +import * as SecureStore from 'expo-secure-store'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { useNavBar } from './NavBarProvider'; +import { useTheme } from './ThemeContext'; const NavBar = () => { - let colorScheme = useColorScheme(); - const styles = getStyles(colorScheme); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const styles = getStyles(currentTheme); + const [accentColour, setAccentColour] = useState('greenyellow'); const { currentTab, setCurrentTab } = useNavBar(); const handleTabPress = (tabName, route) => { setCurrentTab(tabName); router.replace(route); }; + + useEffect(() => { + const getSettings = async () => { + let accentcolour = await SecureStore.getItemAsync('accentColour'); + setAccentColour(accentcolour); + }; + getSettings(); +}, []); // console.log(currentTab); return ( @@ -40,13 +53,13 @@ const NavBar = () => { Home @@ -63,13 +76,13 @@ const NavBar = () => { My bookings @@ -87,13 +100,13 @@ const NavBar = () => { as={CalendarDaysIcon} w={hp('3%')} h={hp('3%')} - color={currentTab === 'Book' ? 'yellowgreen' : colorScheme === 'dark' ? 'white' : 'black'} + color={currentTab === 'Book' ? `${accentColour}` : currentTheme === 'dark' ? 'white' : 'black'} /> Book @@ -111,14 +124,14 @@ const NavBar = () => { as={BellIcon} w={hp('3%')} h={hp('3%')} - color={currentTab === 'Notifications' ? 'yellowgreen' : colorScheme === 'dark' ? 'white' : 'black'} + color={currentTab === 'Notifications' ? `${accentColour}` : currentTheme === 'dark' ? 'white' : 'black'} /> Notifications @@ -135,14 +148,14 @@ const NavBar = () => { Profile @@ -151,7 +164,7 @@ const NavBar = () => { ); }; -const getStyles = (colorScheme) => StyleSheet.create({ +const getStyles = (currentTheme) => StyleSheet.create({ container: { position: 'absolute', bottom: 0, @@ -161,10 +174,10 @@ const getStyles = (colorScheme) => StyleSheet.create({ paddingBottom: hp('3%'), flexDirection: 'row', justifyContent: 'space-around', - backgroundColor: colorScheme === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.5)', + backgroundColor: currentTheme === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.5)', paddingVertical: hp('1%'), borderTopWidth: 1, - borderTopColor: colorScheme === 'dark' ? '#444' : '#ccc', + borderTopColor: currentTheme === 'dark' ? '#444' : '#ccc', borderLeftColor: '#ccc', borderRightColor: '#ccc', } diff --git a/frontend/occupi-mobile4/components/ThemeContext.tsx b/frontend/occupi-mobile4/components/ThemeContext.tsx new file mode 100644 index 00000000..acef058e --- /dev/null +++ b/frontend/occupi-mobile4/components/ThemeContext.tsx @@ -0,0 +1,72 @@ +// ThemeContext.tsx + +import React, { createContext, useState, useEffect, ReactNode } from 'react'; +import * as SecureStore from 'expo-secure-store'; +import { View, ActivityIndicator, useColorScheme } from 'react-native'; + +// Define the shape of the context state +interface ThemeContextType { + theme: string; + setTheme: (theme: string) => void; +} + +// Create the context +const ThemeContext = createContext(undefined); + +// Create a provider component +export const ThemeProvider = ({ children }: { children: ReactNode }) => { + const [theme, setTheme] = useState(null); // Start with null to indicate loading + + useEffect(() => { + const fetchTheme = async () => { + try { + const storedTheme = await SecureStore.getItemAsync('Theme'); + if (storedTheme) { + setTheme(storedTheme); + } else { + // If no theme is found, set a default theme + setTheme('light'); + } + } catch (error) { + console.error('Failed to load theme from SecureStore:', error); + setTheme('light'); // Set a default theme in case of an error + } + }; + + fetchTheme(); + }, []); + + const updateTheme = async (newTheme: string) => { + console.log('updating theme'); + try { + await SecureStore.setItemAsync('Theme', newTheme); + setTheme(newTheme); + } catch (error) { + console.error('Failed to save theme to SecureStore:', error); + } + }; + + if (theme === null) { + // Show a loading spinner while the theme is being fetched + return ( + + + + ); + } + + return ( + + {children} + + ); +}; + +// Custom hook to use the ThemeContext +export const useTheme = (): ThemeContextType => { + const context = React.useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; diff --git a/frontend/occupi-mobile4/components/__tests__/NavBar-test.tsx b/frontend/occupi-mobile4/components/__tests__/NavBar-test.tsx index 223c2694..e5af1344 100644 --- a/frontend/occupi-mobile4/components/__tests__/NavBar-test.tsx +++ b/frontend/occupi-mobile4/components/__tests__/NavBar-test.tsx @@ -1,9 +1,23 @@ import * as React from 'react'; import renderer from 'react-test-renderer'; import NavBar from '../NavBar'; +import { useNavBar } from '../NavBarProvider'; -it(`renders correctly`, () => { - const tree = renderer.create(Snapshot test!).toJSON(); +// Mock the NavBarProvider module +jest.mock('../NavBarProvider', () => ({ + useNavBar: jest.fn(), +})); - expect(tree).toMatchSnapshot(); -}); +describe('NavBar', () => { + it(`renders correctly`, () => { + // Mock the useNavBar hook implementation + (useNavBar as jest.Mock).mockReturnValue({ + currentTab: 'Home', + setCurrentTab: jest.fn(), + }); + + const tree = renderer.create().toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/frontend/occupi-mobile4/components/__tests__/__snapshots__/NavBar-test.tsx.snap b/frontend/occupi-mobile4/components/__tests__/__snapshots__/NavBar-test.tsx.snap index 4687366d..5646babc 100644 --- a/frontend/occupi-mobile4/components/__tests__/__snapshots__/NavBar-test.tsx.snap +++ b/frontend/occupi-mobile4/components/__tests__/__snapshots__/NavBar-test.tsx.snap @@ -1,5 +1,922 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`NavBar renders correctly 1`] = ` + + + + + + Home + + + + + + My bookings + + + + + + + + + + + + + + + + + + + + + Book + + + + + + + + + + + + + Notifications + + + + + + Profile + + + +`; + exports[`renders correctly 1`] = ` { + await SecureStore.setItemAsync('UserData', value); + }, + + storeToken: async (value) => { + await SecureStore.setItemAsync('Token', value); + }, + + storeUserEmail: async (value) => { + await SecureStore.setItemAsync('Email', value); + }, + + getToken: async () => { + return await SecureStore.getItemAsync('Token'); + } +}; + +export default LoginModel; diff --git a/frontend/occupi-mobile4/models/data.ts b/frontend/occupi-mobile4/models/data.ts new file mode 100644 index 00000000..80fc0594 --- /dev/null +++ b/frontend/occupi-mobile4/models/data.ts @@ -0,0 +1,52 @@ +//these models describe the room structure as well as booking information structure + +export interface Room { + description: string; + floorNo: string; + maxOccupancy: string; + minOccupancy:string; + roomId: string; + roomName: string; + roomNo: string; +} + +export interface Booking { + checkedIn: boolean; + creator: string; + date: string; + emails: string[]; + end: string; + floorNo: string; + occupiId: string; + roomId: string; + roomName: string; + start: string; +} + +export interface User { + email: string; + name: string; + dob: string; + gender: "Male" | "Female" | "Other"; + session_email: string; + employeeid: string; + number: string; + pronouns?: string; +} + +export interface Notification { + message: string; + send_time: string; + title: string; + unreadEmails: string[]; +} + +export interface SecuritySettings { + mfa: "on" | "off"; + forceLogout: "on" | "off"; +} + +export interface NotificationSettings { + invites: "on" | "off"; + bookingReminder: "on" | "off"; +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/models/requests.ts b/frontend/occupi-mobile4/models/requests.ts new file mode 100644 index 00000000..b9907cae --- /dev/null +++ b/frontend/occupi-mobile4/models/requests.ts @@ -0,0 +1,127 @@ +//these models describe data to be sent to the api (POST body) + +/* ---Auth Requests--- */ + +export interface LoginReq { + email: string; + password: string; +} + +export interface RegisterReq { + email: string; + password: string; + employee_id?: string; + expoPushToken: string; +} + +export interface VerifyOTPReq { + email: string; + otp: string; +} + +export interface ResetPasswordReq { + email: string; + newPassword: string; + newPasswordConfirm: string; + otp: string; +} + +/* ---API Requests--- */ + +//Rooms & Bookings + +export interface BookRoomReq { + roomId: string; + roomName: string; + emails: string[]; + creator: string; + floorNo: string; //string integer + date: string; + start: string; + end: string; +} + +export interface ViewBookingsReq { + operator: string; + filter: { + email: string; + date?: string; + }; + order_asc?: string; + order_desc?: string; + projection?: string[]; + limit?: number; + page?: number; +} + +export interface ViewRoomsReq { + operator: string; + filter?: { + floorNo: string; + }; + order_asc?: string; + order_desc?: string; + projection?: string[]; + limit?: number; + page?: number; +} + +export interface CancelBookingReq { + bookingId: string; + roomId: string; + emails: string[]; + creator: string; + floorNo: string; //string integer + date: string; + start: string; + end: string; + roomName: string; +} + +export interface CheckInReq { + bookingId: string; + email: string; +} + + +//Users + +export interface UpdateDetailsReq { + email?: string; + name?: string; + dob?: string; + gender?: string; + session_email: string; + employeeid?: string; + number?: string; + pronouns?: string; +} + +export interface NotificationsReq { + operator: string; + filter?: { + emails: string[]; + }; + order_asc?: string; + order_desc?: string; + projection?: string[]; + limit?: number; + page?: number; +} + +//Updating settings + +export interface SecuritySettingsReq { + email: string; + mfa?: "on" | "off"; + forceLogout?: "on" | "off"; + currentPassword?: string; + newPassword?: string; + newPasswordConfirm?: string; +} + +export interface NotificationSettingsReq { + email: string; + invites: "on" | "off"; + bookingReminder: "on" | "off"; +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/models/response.ts b/frontend/occupi-mobile4/models/response.ts new file mode 100644 index 00000000..cf451700 --- /dev/null +++ b/frontend/occupi-mobile4/models/response.ts @@ -0,0 +1,28 @@ +//these models describe the structure of the responses from the api + +export interface Error { + code: string; + details: string; + message: string; +} +export interface Unsuccessful { + data: null; + status: 'error'; + message: string; + error: Error; +} + +export interface LoginSuccess { + data: { + token: string; + }; + message: string; + status: number; +} + +export interface Success { + status: number; + message: string; + data: any; +} + diff --git a/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx b/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx index f3eb6fe7..2df4c26f 100644 --- a/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx +++ b/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx @@ -13,6 +13,7 @@ import Navbar from '../../components/NavBar'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import * as SecureStore from 'expo-secure-store'; import { Skeleton } from 'moti/skeleton'; +import { useTheme } from '@/components/ThemeContext'; const groupDataInPairs = (data) => { if (!data) return []; @@ -36,9 +37,11 @@ interface Room { const BookRoom = () => { const router = useRouter(); - const colorScheme = useColorScheme(); + const { theme } = useTheme(); + const colorscheme = useColorScheme(); const toast = useToast(); - const [isDarkMode, setIsDarkMode] = useState(colorScheme === 'dark'); + const currentTheme = theme === "system" ? colorscheme : theme; + const isDarkMode = currentTheme === "dark"; const [layout, setLayout] = useState("row"); const [loading, setLoading] = useState(true); const [roomData, setRoomData] = useState([]); @@ -103,11 +106,8 @@ const BookRoom = () => { } }; fetchAllRooms(); - }, [toast, apiUrl, viewroomsendpoint]); + }, [toast]); - useEffect(() => { - setIsDarkMode(colorScheme === 'dark'); - }, [colorScheme]); const backgroundColor = isDarkMode ? 'black' : 'white'; const textColor = isDarkMode ? 'white' : 'black'; diff --git a/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx b/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx index da8bb457..dabed24a 100644 --- a/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx +++ b/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx @@ -24,177 +24,76 @@ import { } from 'react-native-responsive-screen'; import PagerView from 'react-native-pager-view'; import { useRouter } from 'expo-router'; - -interface Room { - _id: string; - roomName: string; - roomId: string; - roomNo: number; - floorNo: number; - minOccupancy: number; - maxOccupancy: number; - description: string; - emails: string[]; - date: string; - start: string; - end: string; - creator: string; -} - -const ViewBookingDetails = (bookingId:string, roomName:string) => { - const colorScheme = useColorScheme(); - const isDarkMode = colorScheme === 'dark'; - const [room, setRoom] = useState({}); +import { Booking } from '@/models/data'; +import { userCancelBooking, userCheckin } from '@/utils/bookings'; +import { useTheme } from '@/components/ThemeContext'; + +const ViewBookingDetails = () => { + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const isDarkMode = currentTheme === 'dark'; + const [room, setRoom] = useState(); const router = useRouter(); const [checkedIn, setCheckedIn] = useState(false); const [isLoading, setIsLoading] = useState(false); const toast = useToast(); - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const checkinendpoint = process.env.EXPO_PUBLIC_CHECK_IN; - const cancelbookingendpoint = process.env.EXPO_PUBLIC_CANCEL_BOOKING; // console.log("HERE:" + room); useEffect(() => { const getCurrentRoom = async () => { - let result : string = await SecureStore.getItemAsync('CurrentRoom'); - // console.log("CurrentRoom:",result); - // setUserDetails(JSON.parse(result).data); - let jsonresult = JSON.parse(result); - console.log(jsonresult); - setRoom(jsonresult); - setCheckedIn(jsonresult.checkedIn); + let result: string = await SecureStore.getItemAsync('CurrentRoom'); + // console.log("CurrentRoom:",result); + // setUserDetails(JSON.parse(result).data); + let jsonresult = JSON.parse(result); + // console.log(jsonresult); + setRoom(jsonresult); + setCheckedIn(jsonresult.checkedIn); }; getCurrentRoom(); - }, []); + }, []); + + // console.log("Room",room?._id); - // console.log("Room",room._id); - const checkin = async () => { - const body = { - "bookingId": room._id, - "creator": room.creator - }; setIsLoading(true); - console.log(body); - // console.log(apiUrl+""+checkinendpoint); - let authToken = await SecureStore.getItemAsync('Token'); - try { - const response = await fetch(`${apiUrl}${checkinendpoint}`, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - body: JSON.stringify(body), - credentials: "include" - }); - const data = await response.json(); - // console.log(data); - // const cookies = response.headers.get('Accept'); - // console.log(cookies); - if (response.ok) { - setCheckedIn(true); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - setIsLoading(false); - } else { - setIsLoading(false); - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); + const response = await userCheckin(); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); } - } catch (error) { - console.error('Error:', error); - } + }); + setIsLoading(false); }; const cancelBooking = async () => { - const body = { - "bookingId": room._id, - "creator": room.creator, - "roomId": room.roomId, - "emails": room.emails, - "roomName": room.roomName, - "floorNo": room.floorNo, - "date": room.date, - "start": room.start, - "end": room.end - - }; setIsLoading(true); - console.log(body); - let authToken = await SecureStore.getItemAsync('Token'); - try { - const response = await fetch(`${apiUrl}${cancelbookingendpoint}`, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - body: JSON.stringify(body), - credentials: "include" - }); - const data = await response.json(); - console.log(data); - // const cookies = response.headers.get('Accept'); - // console.log(cookies); - if (response.ok) { - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - setIsLoading(false); - router.replace("/home"); - } else { - setIsLoading(false); - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); + const response = await userCancelBooking(); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); } - } catch (error) { - console.error('Error:', error); - } + }); + setIsLoading(false); }; return ( - + - router.back()} /> - {room.roomName} + router.back()} /> + {room?.roomName} @@ -211,25 +110,25 @@ const ViewBookingDetails = (bookingId:string, roomName:string) => { - {room.roomName} + {room?.roomName} Fast OLED 3 - 5 - Floor: {room.floorNo === 0 ? 'G' : room.floorNo} + Floor: {room?.floorNo === 0 ? 'G' : room?.floorNo} - Attendees: {room.emails?.length} + Attendees: {room?.emails?.length} - {room.emails?.map((email, idx) => ( + {room?.emails?.map((email, idx) => ( {idx + 1}. {email} ))} - Description - The {room.roomName} is a state-of-the-art conference space designed for modern digital connectivity, seating 3-6 comfortably. Equipped with multiple HDMI ports, a high-definition projector or large LED screen, surround sound, and wireless display options, it ensures seamless presentations and video conferencing. The room features an intuitive control panel, high-speed Wi-Fi, and ample power outlets. Additional amenities include whiteboards, flip charts, adjustable lighting, and climate control, all within a professional and comfortable interior designed for productivity. + Description + The {room?.roomName} is a state-of-the-art conference space designed for modern digital connectivity, seating 3-6 comfortably. Equipped with multiple HDMI ports, a high-definition projector or large LED screen, surround sound, and wireless display options, it ensures seamless presentations and video conferencing. The room features an intuitive control panel, high-speed Wi-Fi, and ample power outlets. Additional amenities include whiteboards, flip charts, adjustable lighting, and climate control, all within a professional and comfortable interior designed for productivity. diff --git a/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx b/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx index b140e8ef..caa3e01c 100644 --- a/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx +++ b/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx @@ -14,30 +14,20 @@ import Navbar from '../../components/NavBar'; import * as SecureStore from 'expo-secure-store'; import { useRouter } from 'expo-router'; import { Skeleton } from 'moti/skeleton'; +import { Booking } from '@/models/data'; +import { fetchUserBookings } from '@/utils/bookings'; +import { useTheme } from '@/components/ThemeContext'; + + const groupDataInPairs = (data) => { const pairs = []; - for (let i = 0; i < data.length; i += 2) { - pairs.push(data.slice(i, i + 2)); + for (let i = 0; i < 10; i += 2) { + pairs.push(data?.slice(i, i + 2)); } return pairs; }; -interface Room { - _id: string; - roomName: string; - roomId: string; - roomNo: number; - floorNo: number; - minOccupancy: number; - maxOccupancy: number; - description: string; - emails: string[]; - date: string; - start: string; - end: string; -} - function extractTimeFromDate(dateString: string): string { const date = new Date(dateString); date.setHours(date.getHours() - 2); @@ -50,18 +40,33 @@ function extractDateFromDate(dateString: string): string { } const ViewBookings = () => { - const colorScheme = useColorScheme(); - const [isDarkMode, setIsDarkMode] = useState(colorScheme === 'dark'); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const isDarkMode = currentTheme === "dark"; const [layout, setLayout] = useState("row"); - const toast = useToast(); - const [roomData, setRoomData] = useState([]); + const [roomData, setRoomData] = useState(); // const [selectedSort, setSelectedSort] = useState("newest"); - const [email, setEmail] = useState(''); const router = useRouter(); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const viewbookingsendpoint = process.env.EXPO_PUBLIC_VIEW_BOOKINGS; + useEffect(() => { + const getRoomData = async () => { + try { + const roomData = await fetchUserBookings(); + if (roomData) { + // console.log(roomData); + setRoomData(roomData); + } else { + setRoomData([]); + } + } catch (error) { + console.error('Error fetching bookings:', error); + } + setLoading(false); + }; + getRoomData(); + }, []); const [accentColour, setAccentColour] = useState('greenyellow'); useEffect(() => { @@ -74,138 +79,36 @@ const ViewBookings = () => { const onRefresh = React.useCallback(() => { - const fetchAllRooms = async () => { - console.log("heree"); - let authToken = await SecureStore.getItemAsync('Token'); - console.log("Token:" + authToken); + const getRoomData = async () => { try { - const response = await fetch(`${apiUrl}${viewbookingsendpoint}?email=${email}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - }); - const data = await response.json(); - if (response.ok) { - setRoomData(data.data || []); // Ensure data is an array - setLoading(false); + const roomData = await fetchUserBookings(); + if (roomData) { + // console.log(roomData); + setRoomData(roomData); } else { - console.log(data); - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); + setRoomData([]); // Default value if no username is found } } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error: {error.message} - - ); - }, - }); + console.error('Error fetching bookings:', error); } + setLoading(false); }; setRefreshing(true); setTimeout(() => { setRefreshing(false); - fetchAllRooms(); + getRoomData(); }, 2000); - }, [toast, apiUrl, viewbookingsendpoint, email]); + }, []); const toggleLayout = () => { setLayout((prevLayout) => (prevLayout === "row" ? "grid" : "row")); }; - useEffect(() => { - setIsDarkMode(colorScheme === 'dark'); - }, [colorScheme]); + const backgroundColor = isDarkMode ? 'black' : 'white'; const textColor = isDarkMode ? 'white' : 'black'; const cardBackgroundColor = isDarkMode ? '#2C2C2E' : '#F3F3F3'; - // const data = [ - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // ]; - useEffect(() => { - const fetchAllRooms = async () => { - let authToken = await SecureStore.getItemAsync('Token'); - let result = await SecureStore.getItemAsync('UserData'); - // console.log(result); - // if (result !== undefined) { - let jsonresult = JSON.parse(result); - setEmail(jsonresult?.data?.email); - // } - // console.log("Token:"+authToken); - // console.log("heree"); - try { - // console.log(`${apiUrl}${viewbookingsendpoint}?email=${jsonresult?.data?.email}`); - const response = await fetch(`${apiUrl}${viewbookingsendpoint}?email=${jsonresult?.data?.email}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - }); - const data = await response.json(); - // console.log(data); - if (response.ok) { - setRoomData(data.data || []); // Ensure data is an array - setLoading(false); - } else { - console.log(data); - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error: {error.message} - - ); - }, - }); - } - }; - fetchAllRooms(); - }, [toast, apiUrl, email, viewbookingsendpoint]); + const roomPairs = groupDataInPairs(roomData); @@ -313,8 +216,9 @@ const ViewBookings = () => { marginBottom: 20, }} > - {pair.map((room) => ( + {pair.map((room, idx) => ( handleRoomClick(JSON.stringify(room))} style={{ flex: 1, @@ -341,7 +245,7 @@ const ViewBookings = () => { {room.roomName} - Attendees: {room.emails.length} + Attendees: {room.emails?.length} Your booking time: @@ -366,8 +270,9 @@ const ViewBookings = () => { } > - {roomData.map((room) => ( + {roomData?.map((room, idx) => ( handleRoomClick(JSON.stringify(room))} style={{ flex: 1, @@ -398,7 +303,7 @@ const ViewBookings = () => { > {room.roomName} - Attendees: {room.emails.length} + Attendees: {room.emails?.length} Your booking time: diff --git a/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx b/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx index ff7983d5..7732ed84 100644 --- a/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx +++ b/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { StatusBar, useColorScheme, Dimensions } from 'react-native'; +import { StatusBar, useColorScheme, Dimensions, TouchableOpacity } from 'react-native'; import Navbar from '../../components/NavBar'; import { Text, @@ -11,14 +11,19 @@ import { ToastTitle, Button, ButtonText, + ScrollView, } from '@gluestack-ui/themed'; import { LineChart } from "react-native-chart-kit"; -import { FontAwesome6 } from '@expo/vector-icons'; import * as SecureStore from 'expo-secure-store'; +import { FontAwesome6 } from '@expo/vector-icons'; // import { router } from 'expo-router'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; +import { fetchUsername } from '@/utils/user'; +import { Booking } from '@/models/data'; +import { fetchUserBookings } from '@/utils/bookings'; +import { useTheme } from '@/components/ThemeContext'; // import { number } from 'zod'; const getRandomNumber = () => { @@ -27,12 +32,16 @@ const getRandomNumber = () => { const Dashboard = () => { const colorScheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorScheme : theme; const [numbers, setNumbers] = useState(Array.from({ length: 15 }, getRandomNumber)); - const [isDarkMode, setIsDarkMode] = useState(colorScheme === 'dark'); + const [isDarkMode, setIsDarkMode] = useState(currentTheme === 'dark'); const [checkedIn, setCheckedIn] = useState(false); - const [name, setName] = useState("User"); - const toast = useToast() - const [accentColour, setAccentColour] = useState('greenyellow'); + const [roomData, setRoomData] = useState({}); + const [username, setUsername] = useState(''); + const toast = useToast(); + // console.log(currentTheme); + // console.log(isDarkMode); useEffect(() => { const getAccentColour = async () => { @@ -41,127 +50,53 @@ const Dashboard = () => { }; getAccentColour(); }, []); - useEffect(() => { - const intervalId = setInterval(() => { - setNumbers(prevNumbers => { - const newNumbers = [getRandomNumber(), ...prevNumbers.slice(0, 14)]; - return newNumbers; - }); - }, 3000); - setIsDarkMode(colorScheme === 'dark'); - return () => clearInterval(intervalId); - }, [colorScheme]); useEffect(() => { - const getUserDetails = async () => { - let result = await SecureStore.getItemAsync('UserData'); - console.log(result); - if (result !== undefined) { - let jsonresult = JSON.parse(result); - setName(String(jsonresult?.data?.details?.name)); - } - }; - const getUserSettings = async () => { + const getUsername = async () => { try { - let authToken = await SecureStore.getItemAsync('Token'); - let email = await SecureStore.getItemAsync('Email'); - // console.log(authToken); - const response = await fetch(`https://dev.occupi.tech/api/get-notification-settings?email=${email}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - console.log(data); - if (response.ok) { - const settings = { - invites: data.data.invites, - bookingReminder: data.data.bookingReminder - }; - // console.log(settings); - await SecureStore.setItemAsync('Notifications', JSON.stringify(settings)); + const name = await fetchUsername(); + // console.log(name); + if (name) { + setUsername(name); } else { - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); + setUsername('Guest'); // Default value if no username is found } } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error - - ); - }, - }); + console.error('Error fetching username:', error); + setUsername('Guest'); // Default in case of an error } + }; + + const getRoomData = async () => { try { - let authToken = await SecureStore.getItemAsync('Token'); - let email = await SecureStore.getItemAsync('Email'); - // console.log(authToken); - const response = await fetch(`https://dev.occupi.tech/api/get-security-settings?email=${email}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - console.log(data); - if (response.ok) { - const settings = { - mfa: data.data.mfa, - forcelogout: data.data.forceLogout - }; - console.log(settings); - await SecureStore.setItemAsync('Security', JSON.stringify(settings)); - } else { - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); + const roomData = await fetchUserBookings(); + if (roomData) { + // console.log(roomData); + setRoomData(roomData[0]); + // console.log(roomData[0]); } } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error - - ); - }, - }); + console.error('Error fetching bookings:', error); } - } - getUserSettings(); - getUserDetails(); - }, [toast]); + }; + getRoomData(); + getUsername(); + }, []); + + + const [accentColour, setAccentColour] = useState('greenyellow'); + + + useEffect(() => { + const intervalId = setInterval(() => { + setNumbers(prevNumbers => { + const newNumbers = [getRandomNumber(), ...prevNumbers.slice(0, 14)]; + return newNumbers; + }); + }, 3000); + setIsDarkMode(currentTheme === 'dark'); + return () => clearInterval(intervalId); + }, [currentTheme]); const checkIn = () => { if (checkedIn === false) { @@ -187,125 +122,174 @@ const Dashboard = () => { } }; - // async function saveUserEmail(value) { - // await SecureStore.setItemAsync('email', value); - // } - - - // saveUserEmail('kamogelomoeketse@gmail.com'); + function extractTimeFromDate(dateString: string): string { + const date = new Date(dateString); + date.setHours(date.getHours() - 2); + return date.toTimeString().substring(0, 5); + } + function extractDateFromDate(dateString: string): string { + const date = new Date(dateString); + return date.toDateString(); + } const backgroundColor = isDarkMode ? '#1C1C1E' : 'white'; const textColor = isDarkMode ? 'white' : 'black'; const cardBackgroundColor = isDarkMode ? '#2C2C2E' : '#F3F3F3'; return ( - - - - - - Hi {name} 👋 - - - Welcome to Occupi - + <> + + + + + + Hi {username} 👋 + + + Welcome back to Occupi + + + logo - logo - - - - - {numbers[0]} - - {numbers[0] / 10 + 5}% + + Next booking: + + + image + + {roomData.roomName} + + + + {extractDateFromDate(roomData.date)} + {extractTimeFromDate(roomData.start)}-{extractTimeFromDate(roomData.end)} + + + - - - - - {checkedIn ? ( - - ) : ( - - )} - - {/* + + + Capacity {numbers[0] / 10 + 5}% + {numbers[0]} + Compared to + Yesterday + {/* + {numbers[0] / 10 + 5}% + */} + + + + + {checkedIn ? ( + + ) : ( + + )} + + {/* logo */} - - Occupancy levels - + Occupancy levels + `rgba(0, 0, 0, ${opacity})`, + labelColor: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`, + style: { + borderRadius: 20 + }, + propsForDots: { + r: "0", + strokeWidth: "2", + stroke: "green" } - ] - }} - width={Dimensions.get("window").width - 30} // from react-native - height={220} - // yAxisLabel="" - // yAxisSuffix="k" - yAxisInterval={1} // optional, defaults to 1 - chartConfig={{ - backgroundColor: "white", - backgroundGradientFrom: "yellowgreen", - backgroundGradientTo: "cyan", - decimalPlaces: 0, // optional, defaults to 2dp - color: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`, - labelColor: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`, - style: { - borderRadius: 20 - }, - propsForDots: { - r: "0", - strokeWidth: "2", - stroke: "green" - } - }} - bezier - style={{ - marginVertical: 8, - borderRadius: 16, - }} - /> - + }} + bezier + style={{ + marginVertical: 8, + borderRadius: 16, + }} + /> + + - + ); }; diff --git a/frontend/occupi-mobile4/screens/Dashboard/__tests__/Dashboard-test.tsx b/frontend/occupi-mobile4/screens/Dashboard/__tests__/Dashboard-test.tsx index ae402883..fe7814d5 100644 --- a/frontend/occupi-mobile4/screens/Dashboard/__tests__/Dashboard-test.tsx +++ b/frontend/occupi-mobile4/screens/Dashboard/__tests__/Dashboard-test.tsx @@ -2,9 +2,27 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react-native'; import { StyledProvider, Theme } from '@gluestack-ui/themed'; import Dashboard from '../Dashboard'; +import { useNavBar } from '../../../components/NavBarProvider'; // Adjust the path as needed jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); // To prevent warnings about Animated module +jest.mock('react-native/Libraries/Settings/Settings', () => ({ + get: jest.fn(), + set: jest.fn(), +})); + +jest.mock('react-native', () => ({ + ...jest.requireActual('react-native'), + useColorScheme: () => 'light', +})); + +jest.mock('../../../components/NavBarProvider', () => ({ + useNavBar: () => ({ + currentTab: 'Dashboard', + setCurrentTab: jest.fn(), + }), +})); + jest.mock('@gluestack-ui/themed', () => ({ ...jest.requireActual('@gluestack-ui/themed'), useToast: () => ({ @@ -47,8 +65,8 @@ describe('Dashboard component', () => { // Mock SecureStore getItemAsync to resolve with the mocked data require('expo-secure-store').getItemAsync.mockResolvedValueOnce(JSON.stringify(mockedData)); - it('renders text correctly', () => { - const { getByText } = renderWithProvider(); + it('renders text correctly', async () => { + const { getByText } = await renderWithProvider(); expect(getByText('Welcome to Occupi')).toBeTruthy(); }); diff --git a/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx b/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx index c02b09ca..5dcf5414 100644 --- a/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx +++ b/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx @@ -2,20 +2,21 @@ import React, { useRef, useState, useEffect } from 'react'; import { View, StyleSheet, TextInput } from 'react-native'; import { VStack, Box, HStack, Image, Heading, Toast, useToast, ToastTitle, Text, } from '@gluestack-ui/themed'; // import { useForm } from 'react-hook-form'; -// import { z } from 'zod'; +import { z } from 'zod'; // import { zodResolver } from '@hookform/resolvers/zod'; import * as SecureStore from 'expo-secure-store'; -import { useRouter, useLocalSearchParams } from 'expo-router'; +import { useRouter, useLocalSearchParams } from 'expo-router'; import Logo from './assets/images/Occupi/file.png'; import StyledExpoRouterLink from '@/components/StyledExpoRouterLink'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { LinearGradient } from 'expo-linear-gradient'; +import { VerifyUserOtpLogin, verifyUserOtpRegister } from '@/utils/auth'; -// const OTPSchema = z.object({ -// OTP: z.string().min(6, 'OTP must be at least 6 characters in length'), -// }); +const OTPSchema = z.object({ + OTP: z.string().min(6, 'OTP must be at least 6 characters in length'), +}); -// type OTPSchemaType = z.infer; +type OTPSchemaType = z.infer; const OTPVerification = () => { const [email, setEmail] = useState(""); @@ -24,12 +25,9 @@ const OTPVerification = () => { const otpSent = useState(false); const timerRef = useRef(null); const [loading, setLoading] = useState(false); - const router = useRouter(); const toast = useToast(); const [otp, setOtp] = useState(['', '', '', '', '', '']); - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const getUserDetailsUrl = process.env.EXPO_PUBLIC_GET_USER_DETAILS; - // console.log(email); + const [state, setState] = useState(); useEffect(() => { if (remainingTime > 0 && !otpSent) { @@ -49,20 +47,13 @@ const OTPVerification = () => { useEffect(() => { const getUserEmail = async () => { let email = await SecureStore.getItemAsync('Email'); - // console.log("email",email); + const state = await SecureStore.getItemAsync('AppState'); + setState(state); setEmail(email); }; getUserEmail(); }, []); - async function storeToken(value) { - await SecureStore.setItemAsync('Token', value); - } - - async function storeUserData(value) { - await SecureStore.setItemAsync('UserData', value); - } - // console.log("here",email); const onSubmit = async () => { @@ -74,99 +65,33 @@ const OTPVerification = () => { // return; // } // setValidationError(null); - console.log(pin); setLoading(true); - try { - const response = await fetch('https://dev.occupi.tech/auth/verify-otp-mobile-login', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email: email, - otp: pin - }), - credentials: "include" + if (state === 'verify_otp_register') { + const response = await verifyUserOtpRegister(email, pin); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); + } + }); + } + else { + const response = await VerifyUserOtpLogin(email, pin); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); + } }); - const data = await response.json(); - if (response.ok) { - setLoading(false); - // console.log(data.data.token); - storeToken(data.data.token); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - try { - let authToken = await SecureStore.getItemAsync('Token'); - console.log(authToken); - const response = await fetch(`${apiUrl}${getUserDetailsUrl}?email=${email}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - // console.log("here"); - if (response.ok) { - storeUserData(JSON.stringify(data)); - // console.log(`Data of ${email}: `, data); - } else { - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error - - ); - }, - }); - } - router.replace('/home'); - } else { - setLoading(false); - // console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); - // setResponse('An error occurred'); } - // }, 3000); setLoading(false); }; @@ -185,41 +110,41 @@ const OTPVerification = () => { ); return ( - - - - + + + + Entered OTP: {otp.join('')} - {remainingTime} seconds remaining - {loading ? ( - - ) : ( - {remainingTime} seconds remaining + {loading ? ( + - )} - - - - - + text="Verifying OTP..." + /> + ) : ( + + )} + + + + + ); }; @@ -335,7 +260,7 @@ const OTPInput = ({ otp, setOtp }) => { keyboardType="numeric" maxLength={1} ref={(ref) => inputRefs.current[index] = ref} - // autoFocus={index === inputRefs.current[index]} // Auto focus the first input on mount + // autoFocus={index === inputRefs.current[index]} // Auto focus the first input on mount /> ))} diff --git a/frontend/occupi-mobile4/screens/Login/SetDetails.tsx b/frontend/occupi-mobile4/screens/Login/SetDetails.tsx index 53101689..007a9460 100644 --- a/frontend/occupi-mobile4/screens/Login/SetDetails.tsx +++ b/frontend/occupi-mobile4/screens/Login/SetDetails.tsx @@ -27,6 +27,8 @@ import { useColorScheme } from 'react-native'; import { heightPercentageToDP as hp } from 'react-native-responsive-screen'; import GradientButton from '@/components/GradientButton'; import LoadingGradientButton from '@/components/LoadingGradientButton'; +import { updateDetails } from '@/utils/user'; +import { extractDateFromTimestamp } from '@/utils/utils'; const COLORS = { white: '#FFFFFF', @@ -47,20 +49,14 @@ const SIZES = { }; const SetDetails = () => { - const [selectedGenderIndex, setSelectedGenderIndex] = useState(1); + const [selectedGenderIndex, setSelectedGenderIndex] = useState(''); const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const [employeeId, setEmployeeId] = useState(''); const [phoneNumber, setPhoneNumber] = useState(''); const [pronouns, setPronouns] = useState(''); const [date, setDate] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isDatePickerVisible, setDatePickerVisibility] = useState(false); let colorScheme = 'light'; - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const getUserDetailsUrl= process.env.EXPO_PUBLIC_GET_USER_DETAILS; - const updateDetailsUrl = process.env.EXPO_PUBLIC_UPDATE_USER_DETAILS; - console.log(apiUrl, getUserDetailsUrl, updateDetailsUrl); const showDatePicker = () => { setDatePickerVisibility(true); @@ -70,78 +66,18 @@ const SetDetails = () => { setDatePickerVisibility(false); }; - const handleConfirm = (selectedDate) => { - setDate(selectedDate); + const handleConfirm = (selectedDate: string) => { + console.log('selected',extractDateFromTimestamp(selectedDate)); + setDate(extractDateFromTimestamp(selectedDate)); hideDatePicker(); }; const onSave = async () => { - const body = { - "email": email, - "details": { - "contactNo": phoneNumber, - "gender": "Male", - "name": name, - "pronouns": pronouns - } - }; - // console.log(JSON.stringify(body)); setIsLoading(true); - try { - let authToken = await SecureStore.getItemAsync('Token'); - const response = await fetch(`${apiUrl}${updateDetailsUrl}`, { - method: 'PUT', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - body: JSON.stringify(body), - credentials: "include" - }); - const data = await response.json(); - console.log(data); - if (response.ok) { - console.log(response); - setIsLoading(false); - alert('Details updated successfully'); - } else { - console.log(data); - setIsLoading(false); - } - } catch (error) { - setIsLoading(false); - console.error('Error:', error); - // setResponse('An error occurred'); - } - - try { - let authToken = await SecureStore.getItemAsync('Token'); - const response = await fetch(`${apiUrl}${getUserDetailsUrl}?email=${email}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - if (response.ok) { - saveUserData(JSON.stringify(data)); - console.log(data); - } else { - console.log(data); - } - } catch (error) { - console.error('Error:', error); - } + const response = await updateDetails(name,date,selectedGenderIndex,phoneNumber,pronouns) + console.log(response); + setIsLoading(false); }; - console.log(selectedGenderIndex); - - async function saveUserData(value) { - await SecureStore.setItemAsync('UserData', value); - } return ( { > - router.replace('/settings')} - /> - Account Details + My account { style={styles.icon} /> - Full name { /> Gender - setSelectedGenderIndex(index)}> + setSelectedGenderIndex(index)}> { borderColor="#f2f2f2" h={hp('5%')} px="$4" + > Male @@ -237,7 +166,6 @@ const SetDetails = () => { - Cell No { ) : ( ) } - diff --git a/frontend/occupi-mobile4/screens/Login/SignIn.tsx b/frontend/occupi-mobile4/screens/Login/SignIn.tsx index 231f4803..d938634b 100644 --- a/frontend/occupi-mobile4/screens/Login/SignIn.tsx +++ b/frontend/occupi-mobile4/screens/Login/SignIn.tsx @@ -37,10 +37,10 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { AlertTriangle, EyeIcon, EyeOffIcon } from 'lucide-react-native'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; -import * as SecureStore from 'expo-secure-store'; import Logo from '../../screens/Login/assets/images/Occupi/file.png'; import StyledExpoRouterLink from '../../components/StyledExpoRouterLink'; import GradientButton from '@/components/GradientButton'; +import { UserLogin } from '@/utils/auth'; const signInSchema = z.object({ email: z.string().min(1, 'Email is required').email(), @@ -67,9 +67,6 @@ const SignInForm = () => { } = useForm({ resolver: zodResolver(signInSchema), }); - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const loginUrl = process.env.EXPO_PUBLIC_LOGIN; - const getUserDetailsUrl = process.env.EXPO_PUBLIC_GET_USER_DETAILS; const isEmailFocused = useState(false); const [loading, setLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); @@ -87,19 +84,6 @@ const SignInForm = () => { // console.log('Biometric hardware available:', isBiometricAvailable); }; - async function storeUserData(value) { - await SecureStore.setItemAsync('UserData', value); - } - - async function storeToken(value) { - await SecureStore.setItemAsync('Token', value); - } - - async function storeUserEmail(value) { - await SecureStore.setItemAsync('Email', value); - } - - const handleBiometricSignIn = async () => { const biometricType = await LocalAuthentication.supportedAuthenticationTypesAsync(); console.log('Supported biometric types:', biometricType); @@ -114,7 +98,7 @@ const SignInForm = () => { }); console.log('Biometric authentication result:', result); if (result.success) { - router.replace('/home'); + // router.replace('/home'); } else { console.log('Biometric authentication failed'); toast.show({ @@ -160,104 +144,17 @@ const SignInForm = () => { const onSubmit = async (_data: SignInSchemaType) => { setLoading(true); - storeUserEmail(_data.email); - const body = { - email: _data.email, - password: _data.password - }; - console.log(body); - try { - const response = await fetch(`${apiUrl}${loginUrl}`, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(body), - credentials: "include" - }); - const data = await response.json(); - if (response.ok) { - console.log("Data here", data); - setLoading(false); - if (data.data) { - storeToken(data.data.token); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - try { - let authToken = await SecureStore.getItemAsync('Token'); - // console.log(authToken); - const response = await fetch(`${apiUrl}${getUserDetailsUrl}?email=${_data.email}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - console.log("here"); - if (response.ok) { - storeUserData(JSON.stringify(data)); - console.log(`Data of ${_data.email}: `, data); - } else { - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error - - ); - }, - }); - } - router.replace('/home'); - } - else { - setLoading(false); - router.replace('/verify-otp'); - } - } else { - setLoading(false); - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); + const response = await UserLogin(_data.email, _data.password); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); } - } catch (error) { - console.error('Error:', error); - } + }); setLoading(false); }; diff --git a/frontend/occupi-mobile4/screens/Login/SignUp.tsx b/frontend/occupi-mobile4/screens/Login/SignUp.tsx index 874d99ba..30e09d82 100644 --- a/frontend/occupi-mobile4/screens/Login/SignUp.tsx +++ b/frontend/occupi-mobile4/screens/Login/SignUp.tsx @@ -29,6 +29,7 @@ import { FormControlLabelText, View } from '@gluestack-ui/themed'; +import { retrievePushToken } from '@/utils/notifications'; import GradientButton from '@/components/GradientButton'; import { Controller, useForm } from 'react-hook-form'; import { AlertTriangle, EyeIcon, EyeOffIcon } from 'lucide-react-native'; @@ -39,6 +40,7 @@ import { Keyboard } from 'react-native'; import StyledExpoRouterLink from '../../components/StyledExpoRouterLink'; import { router } from 'expo-router'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; +import { userRegister } from '@/utils/auth'; const isEmployeeIdFocused = false; const signUpSchema = z.object({ @@ -69,6 +71,7 @@ const signUpSchema = z.object({ type SignUpSchemaType = z.infer; +retrievePushToken(); const SignUpForm = () => { const { @@ -85,50 +88,18 @@ const SignUpForm = () => { const onSubmit = async (_data: SignUpSchemaType) => { if (_data.password === _data.confirmpassword) { setLoading(true); - try { - const response = await fetch('https://dev.occupi.tech/auth/register', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email: _data.email, - password: _data.password - }), - credentials: "include" - }); - const data = await response.json(); - if (response.ok) { - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - router.push({pathname:'/verify-otp', params: { email: _data.email}}); - } else { - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); + const response = await userRegister(_data.email, _data.password, _data.employeeId); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); } - setLoading(false) + }); + setLoading(false); } else { toast.show({ placement: 'bottom right', diff --git a/frontend/occupi-mobile4/screens/Login/SplashScreen.tsx b/frontend/occupi-mobile4/screens/Login/SplashScreen.tsx index b0902ecc..3cb20b4f 100644 --- a/frontend/occupi-mobile4/screens/Login/SplashScreen.tsx +++ b/frontend/occupi-mobile4/screens/Login/SplashScreen.tsx @@ -93,7 +93,7 @@ export default function SplashScreen() { useEffect(() => { const timer = setTimeout(() => { setSelectedIndex(1); // Assuming Onboarding1 is at index 1 - router.replace('/set-details'); // Navigate to Onboarding1 screen + router.replace('/welcome'); // Navigate to Onboarding1 screen }, 5000); // 8 seconds return () => clearTimeout(timer); // Clean up timer on component unmount diff --git a/frontend/occupi-mobile4/screens/Login/__tests__/Welcome-test.tsx b/frontend/occupi-mobile4/screens/Login/__tests__/Welcome-test.tsx index 704749b7..6192afb4 100644 --- a/frontend/occupi-mobile4/screens/Login/__tests__/Welcome-test.tsx +++ b/frontend/occupi-mobile4/screens/Login/__tests__/Welcome-test.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react-native'; import { StyledProvider, Theme } from '@gluestack-ui/themed'; -import Welcome from '../Welcome'; // Adjust the path to your component +import Welcome from '../Welcome'; +import GradientButton from '@/components/GradientButton'; jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); // To prevent warnings about Animated module jest.mock('expo-router', () => ({ @@ -11,6 +12,7 @@ jest.mock('expo-router', () => ({ }, })); + const renderWithProvider = (component) => { return render( diff --git a/frontend/occupi-mobile4/screens/Notifications/Notifications.tsx b/frontend/occupi-mobile4/screens/Notifications/Notifications.tsx index b663551c..fc21b983 100644 --- a/frontend/occupi-mobile4/screens/Notifications/Notifications.tsx +++ b/frontend/occupi-mobile4/screens/Notifications/Notifications.tsx @@ -14,11 +14,15 @@ import { StatusBar, useColorScheme, Dimensions } from 'react-native'; import { AntDesign, Entypo, FontAwesome6 } from '@expo/vector-icons'; import { Skeleton } from 'moti/skeleton'; import axios from 'axios'; +import { useTheme } from '@/components/ThemeContext'; +import { getUserNotifications } from '@/utils/notifications'; const Notifications = () => { - const colorScheme = useColorScheme(); - const toast = useToast(); - const [notifications, setNotifications] = useState(); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const [accentColour, setAccentColour] = useState('greenyellow'); + const currentTheme = theme === "system" ? colorscheme : theme; + const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(true); const todayNotifications = []; const yesterdayNotifications = []; @@ -26,17 +30,25 @@ const Notifications = () => { const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; + useEffect(() => { + const getSettings = async () => { + let accentcolour = await SecureStore.getItemAsync('accentColour'); + setAccentColour(accentcolour); + }; + getSettings(); + }, []); + const formatNotificationDate = (sendTime) => { const now = new Date(); // console.log(now); const notificationDate = new Date(sendTime); - console.log(notificationDate); + // console.log(notificationDate); const differenceInHours = Math.floor((now - notificationDate) / (1000 * 60 * 60)); const differenceInDays = Math.floor(differenceInHours / 24); if (differenceInDays === 0) { - console.log(differenceInDays); + // console.log(differenceInDays); return differenceInHours < 1 ? 'less than an hour ago' : `${differenceInHours} hours ago`; } else if (differenceInDays === 1) { return 'yesterday'; @@ -46,9 +58,10 @@ const Notifications = () => { }; if (notifications) { + console.log('yurpp'); notifications.forEach(notification => { const formattedDate = formatNotificationDate(notification.send_time); - + if (formattedDate.includes('hours ago') || formattedDate.includes('hour ago')) { todayNotifications.push(notification); } else if (formattedDate === 'yesterday') { @@ -62,67 +75,20 @@ const Notifications = () => { useEffect(() => { const getNotifications = async () => { - let userEmail = await SecureStore.getItemAsync('Email'); - let authToken = await SecureStore.getItemAsync('Token'); - - try { - const response = await axios.get('https://dev.occupi.tech/api/get-notifications', { - params: { - filter: { - emails: [{ userEmail }] - }, - order_desc: "send_time" - }, - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - withCredentials: true - }); - const data = response.data; - // console.log(`Response Data: ${JSON.stringify(data.data)}`); - console.log(data); - if (response.status === 200) { - setNotifications(data.data || []); // Ensure data is an array - setLoading(false); - } else { - console.log(data); - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error: {error.message} - - ); - }, - }); - } + const notifications = await getUserNotifications(); + // console.log(notifications); + setNotifications(notifications); + setLoading(false); }; getNotifications(); - }, [apiUrl, toast]) + }, []) const renderNotifications = (notificationList) => ( notificationList.map((notification, idx) => ( - - + + {notification.message} · {formatNotificationDate(notification.send_time)} @@ -132,10 +98,10 @@ const Notifications = () => { return ( - + - Notifications - + Notifications + @@ -143,20 +109,20 @@ const Notifications = () => { <> {Array.from({ length: 8 }, (_, index) => ( - + ))} ) : ( - Recent + Recent {renderNotifications(todayNotifications)} - Yesterday + Yesterday {renderNotifications(yesterdayNotifications)} - Older + Older {renderNotifications(olderNotifications)} diff --git a/frontend/occupi-mobile4/screens/Office/BookingDetails.tsx b/frontend/occupi-mobile4/screens/Office/BookingDetails.tsx index e978bd23..207064d7 100644 --- a/frontend/occupi-mobile4/screens/Office/BookingDetails.tsx +++ b/frontend/occupi-mobile4/screens/Office/BookingDetails.tsx @@ -26,8 +26,9 @@ import { ActivityIndicator } from 'react-native'; import * as LocalAuthentication from "expo-local-authentication"; import * as SecureStore from 'expo-secure-store'; import GradientButton from '@/components/GradientButton'; - -import { sendPushNotification } from "@/utils/utils"; +import { sendPushNotification } from "@/utils/notifications"; +import { userBookRoom } from "@/utils/bookings"; +import { useTheme } from "@/components/ThemeContext"; const BookingDetails = () => { const navigation = useNavigation(); @@ -35,23 +36,19 @@ const BookingDetails = () => { const [email, setEmail] = useState(""); const [loading, setLoading] = useState(false); const [bookingInfo, setbookingInfo] = useState(); - const colorScheme = useColorScheme(); + const colorscheme = useColorScheme(); const toast = useToast(); const router = useRouter(); - const [creatorEmail, setCreatorEmail] = useState(''); const [startTime, setStartTime] = useState(''); const [endTime, setEndTime] = useState(''); - const isDark = colorScheme === "dark"; + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const isDark = colorscheme === "dark"; const [pushTokens, setPushTokens] = useState([]); - // console.log(creatorEmail + roomId + floorNo); - // console.log(bookingInfo?); - // console.log(startTime); const [attendees, setAttendees] = useState(['']); // console.log(attendees); const cardBackgroundColor = isDark ? '#2C2C2E' : '#F3F3F3'; const steps = ["Booking details", "Invite attendees", "Receipt"]; - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const bookroomendpoint = process.env.EXPO_PUBLIC_BOOK_ROOM; const [accentColour, setAccentColour] = useState('greenyellow'); useEffect(() => { @@ -64,21 +61,13 @@ const BookingDetails = () => { useEffect(() => { const getbookingInfo = async () => { - let userinfo = await SecureStore.getItemAsync('UserData'); - // if (result !== undefined) { - let jsoninfo = JSON.parse(userinfo); - console.log("data", jsoninfo?.data.details.name); - setCreatorEmail(jsoninfo?.data?.email); + let userEmail = await SecureStore.getItemAsync('Email'); let result: string = await SecureStore.getItemAsync('BookingInfo'); - console.log("CurrentRoom:", jsoninfo?.data?.email); - // setUserDetails(JSON.parse(result).data); let jsonresult = JSON.parse(result); - console.log("BookingInfo", jsonresult); setbookingInfo(jsonresult); setStartTime(jsonresult.startTime); setEndTime(jsonresult.endTime); - console.log(jsoninfo?.data?.email); - setAttendees([jsoninfo?.data?.email]); + setAttendees([userEmail]); }; getbookingInfo(); }, []); @@ -95,92 +84,23 @@ const BookingDetails = () => { }; const onSubmit = async () => { - const body = { - "roomId": bookingInfo?.roomId, - "emails": attendees, - "roomName": bookingInfo?.roomName, - "creator": creatorEmail, - "floorNo": bookingInfo?.floorNo, - "date": `${bookingInfo?.date}T00:00:00.000+00:00`, - "start": `${bookingInfo?.date}T${startTime}:00.000+00:00`, - "end": `${bookingInfo?.date}T${endTime}:00.000+00:00` - }; - console.log("hereeeeee", body); - let authToken = await SecureStore.getItemAsync('Token'); - let userinfo = await SecureStore.getItemAsync('UserData'); - let jsoninfo = JSON.parse(userinfo); - try { - setLoading(true); - const response = await fetch(`${apiUrl}${bookroomendpoint}`, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}`, - 'X-Timezone': 'Africa/Johannesburg' - }, - body: JSON.stringify(body), - credentials: "include" - }); - const data = await response.json(); - console.log(data); - console.log(attendees); - if (response.ok) { - try { - const response = await fetch(`${apiUrl}/api/get-push-tokens?emails=${attendees.slice(1)}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}`, - 'X-Timezone': 'Africa/Johannesburg' - }, - credentials: "include" - }); - const data = await response.json(); - console.log("PUSHH TOKENSS",data); - if (data.data) { - let tokens = data.data.map((item) => item.expoPushToken); - setPushTokens(tokens); - console.log(tokens); - sendPushNotification(tokens, "New Booking", `${jsoninfo?.data.details.name} has invited you to a booking.`); - } - setCurrentStep(2); - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - } catch (error) { - setLoading(false); - console.error('Error:', error); - } - } else { - console.log(data); - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); + setLoading(true); + const response = await userBookRoom(attendees, startTime, endTime); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); } - } catch (error) { - setLoading(false); - console.error('Error:', error); - // setResponse('An error occurred'); + }); + + if (response === 'Successfully booked!') { + setCurrentStep(2); } - // }, 3000); + setLoading(false); }; const renderAttendee = ({ item }) => ( @@ -314,9 +234,9 @@ const BookingDetails = () => { }} > navigation.goBack()}> - router.back()} /> + router.back()} /> - {/* */} + {/* */} { @@ -61,8 +39,10 @@ const OfficeDetails = () => { const [startTime, setStartTime] = useState(); const [endTime, setEndTime] = useState(); const router = useRouter(); - const colorScheme = useColorScheme(); - const isDarkMode = colorScheme === 'dark'; + const { theme } = useTheme(); + const colorscheme = useColorScheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const isDarkMode = currentTheme === 'dark'; const [room, setRoom] = useState(); const navigation = useNavigation>(); const scrollX = useRef(new Animated.Value(0)).current; @@ -91,7 +71,7 @@ const OfficeDetails = () => { useEffect(() => { const getCurrentRoom = async () => { let result : string = await SecureStore.getItemAsync('CurrentRoom'); - console.log("CurrentRoom:",result); + // console.log("CurrentRoom:",result); // setUserDetails(JSON.parse(result).data); let jsonresult = JSON.parse(result); // console.log(jsonresult); @@ -134,27 +114,27 @@ const OfficeDetails = () => { maxOccupancy: room?.maxOccupancy }; - console.log(bookingInfo); + // console.log(bookingInfo); await SecureStore.setItemAsync('BookingInfo', JSON.stringify(bookingInfo)); router.replace('/booking-details'); } - + console.log(theme); // console.log(room?); // console.log(userEmail); return ( <> {/* Top Section */} - - navigation.goBack()} /> + + navigation.goBack()} /> {room?.roomName} - + diff --git a/frontend/occupi-mobile4/screens/Settings/Appearance.tsx b/frontend/occupi-mobile4/screens/Settings/Appearance.tsx index 07b95a46..dc6aee5e 100644 --- a/frontend/occupi-mobile4/screens/Settings/Appearance.tsx +++ b/frontend/occupi-mobile4/screens/Settings/Appearance.tsx @@ -3,7 +3,8 @@ import { StyleSheet, Alert, TextInput, - TouchableOpacity + TouchableOpacity, + useColorScheme } from 'react-native'; import { Feather } from '@expo/vector-icons'; import { MaterialCommunityIcons, FontAwesome } from '@expo/vector-icons'; @@ -15,10 +16,11 @@ import { Box } from '@gluestack-ui/themed'; import { router } from 'expo-router'; -import { useColorScheme, Switch } from 'react-native'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import GradientButton from '@/components/GradientButton'; import * as SecureStore from 'expo-secure-store'; +import { storeTheme, storeAccentColour } from '@/services/securestore'; +import { useTheme } from '@/components/ThemeContext'; const FONTS = { h3: { fontSize: 20, fontWeight: 'bold' }, @@ -32,30 +34,25 @@ const SIZES = { }; const Appearance = () => { - //retrieve user settings ad assign variables accordingly - const onSave = () => { - //integration here - }; const [accentColour, setAccentColour] = useState('greenyellow'); - const [theme, setTheme] = useState(''); - let colorScheme = theme; + const { theme, setTheme } = useTheme(); + const colorscheme = useColorScheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + + const onSave = () => { + storeAccentColour(accentColour); + storeTheme(theme); + router.back(); + } useEffect(() => { - const getAccentColour = async () => { + const getSettings = async () => { let accentcolour = await SecureStore.getItemAsync('accentColour'); setAccentColour(accentcolour); }; - getAccentColour(); + getSettings(); }, []); - console.log(accentColour); - - const setAccentcolour = async (value) => { - setAccentColour(value); - await SecureStore.setItemAsync('accentColour', value); - } - // setAccentcolour(); - const handleBack = () => { // if (isSaved === false) { // Alert.alert( @@ -76,44 +73,44 @@ const Appearance = () => { router.back(); // } } - console.log(theme); + // console.log(theme); return ( - + - + Appearance - Mode - + Mode + setTheme("light")} style={{ width: wp('25%') }}> white - Light + Light setTheme("dark")} style={{ width: wp('25%') }}> @@ -121,13 +118,13 @@ const Appearance = () => { white - Dark + Dark setTheme("system")} style={{ width: wp('25%') }}> @@ -135,90 +132,90 @@ const Appearance = () => { white - System + System - Accent colour - + Accent colour + - setAccentcolour("lightgrey")}> + setAccentColour("lightgrey")}> - + - setAccentcolour("#FF4343")}> + setAccentColour("#FF4343")}> - + - setAccentcolour("#FFB443")}> + setAccentColour("#FFB443")}> - + - setAccentcolour("greenyellow")}> + setAccentColour("greenyellow")}> - + - setAccentcolour("#43FF61")}> + setAccentColour("#43FF61")}> - + - setAccentcolour("#43F4FF")}> + setAccentColour("#43F4FF")}> - + - setAccentcolour("#4383FF")}> + setAccentColour("#4383FF")}> - + - setAccentcolour("#AC43FF")}> + setAccentColour("#AC43FF")}> - + - setAccentcolour("#FF43F7")}> + setAccentColour("#FF43F7")}> - + - setAccentcolour("purple")}> - setAccentcolour("#FF4343")}> - + setAccentColour("purple")}> + setAccentColour("#FF4343")}> + - Or enter a custom colour + Or enter a custom colour - Custom colour: + Custom colour: diff --git a/frontend/occupi-mobile4/screens/Settings/ChangePassword.tsx b/frontend/occupi-mobile4/screens/Settings/ChangePassword.tsx index 9c7a4be7..564e50a1 100644 --- a/frontend/occupi-mobile4/screens/Settings/ChangePassword.tsx +++ b/frontend/occupi-mobile4/screens/Settings/ChangePassword.tsx @@ -6,7 +6,8 @@ import { Alert, Keyboard, KeyboardAvoidingView, - Platform + Platform, + useColorScheme } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Feather } from '@expo/vector-icons'; @@ -29,7 +30,6 @@ import { import { Controller, useForm } from 'react-hook-form'; import { router } from 'expo-router'; import { AlertTriangle, EyeIcon, EyeOffIcon } from 'lucide-react-native'; -import { useColorScheme, Switch } from 'react-native'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import GradientButton from '@/components/GradientButton'; @@ -37,6 +37,8 @@ import * as SecureStore from 'expo-secure-store'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import axios from 'axios'; import { Toast, ToastTitle, useToast } from '@gluestack-ui/themed'; +import { updateSecurity } from '@/utils/user'; +import { useTheme } from '@/components/ThemeContext'; const COLORS = { white: '#FFFFFF', @@ -59,67 +61,29 @@ const SIZES = { type SignUpSchemaType = z.infer; const ChangePassword = () => { - let colorScheme = useColorScheme(); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; const toast = useToast(); const onSubmit = async (_data: SignUpSchemaType) => { - //integration here - let userEmail = await SecureStore.getItemAsync('Email'); - console.log(JSON.stringify({ - email: userEmail, + if (_data.password === _data.confirmpassword) { + const settings = { currentPassword: _data.currentpassword, newPassword: _data.password, newPasswordConfirm: _data.confirmpassword - })); - if (_data.password === _data.confirmpassword) { - let userEmail = await SecureStore.getItemAsync('Email'); - let authToken = await SecureStore.getItemAsync('Token'); - - try { - const response = await axios.post('https://dev.occupi.tech/api/update-security-settings', { - email: userEmail, - currentPassword: _data.currentpassword, - newPassword: _data.password, - newPasswordConfirm: _data.confirmpassword - }, { - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - withCredentials: true - }); - const data = response.data; - // console.log(`Response Data: ${JSON.stringify(data.data)}`); - console.log(data); - if (response.status === 200) { - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Password successfully changed - - ); - }, - }); - router.replace('/settings'); - } else { - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - console.log(data); - } - } catch (error) { - console.error('Error:', error); - } + }; + const response = await updateSecurity('password', settings) + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); + }, + }); } else if (_data.currentpassword === _data.password) { Alert.alert('Error', 'New password cannot be the same as the current password'); @@ -189,7 +153,7 @@ const ChangePassword = () => { return ( - + { as={Feather} name="chevron-left" size="xl" - color={colorScheme === 'dark' ? 'white' : 'black'} + color={currentTheme === 'dark' ? 'white' : 'black'} onPress={() => router.back()} /> - + Change Password @@ -218,7 +182,7 @@ const ChangePassword = () => { - Current Password + Current Password { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { onBlur={onBlur} onSubmitEditing={handleKeyPress} returnKeyType="done" - color={colorScheme === 'dark' ? 'white' : 'black'} + color={currentTheme === 'dark' ? 'white' : 'black'} type={showPassword ? 'text' : 'password'} /> @@ -265,7 +229,7 @@ const ChangePassword = () => { - New Password + New Password { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { onChangeText={onChange} onBlur={onBlur} onSubmitEditing={handleKeyPress} - color={colorScheme === 'dark' ? 'white' : 'black'} + color={currentTheme === 'dark' ? 'white' : 'black'} returnKeyType="done" type={showPassword ? 'text' : 'password'} /> @@ -312,7 +276,7 @@ const ChangePassword = () => { - Confirm Password + Confirm Password { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { onChangeText={onChange} onBlur={onBlur} onSubmitEditing={handleKeyPress} - color={colorScheme === 'dark' ? 'white' : 'black'} + color={currentTheme === 'dark' ? 'white' : 'black'} returnKeyType="done" type={showConfirmPassword ? 'text' : 'password'} /> diff --git a/frontend/occupi-mobile4/screens/Settings/FAQPage.tsx b/frontend/occupi-mobile4/screens/Settings/FAQPage.tsx index dbe72326..78c91732 100644 --- a/frontend/occupi-mobile4/screens/Settings/FAQPage.tsx +++ b/frontend/occupi-mobile4/screens/Settings/FAQPage.tsx @@ -1,10 +1,13 @@ import React from 'react'; import { ScrollView, useColorScheme } from 'react-native'; import { View, Text, Accordion, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent } from '@gluestack-ui/themed'; +import { useTheme } from '@/components/ThemeContext'; const FAQPage = () => { - const colorScheme = useColorScheme(); - const isDarkMode = colorScheme === 'dark'; + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const isDarkMode = currentTheme === 'dark'; const faqData = [ { diff --git a/frontend/occupi-mobile4/screens/Settings/Notifications.tsx b/frontend/occupi-mobile4/screens/Settings/Notifications.tsx index cd82dde9..4ba2ad4b 100644 --- a/frontend/occupi-mobile4/screens/Settings/Notifications.tsx +++ b/frontend/occupi-mobile4/screens/Settings/Notifications.tsx @@ -17,6 +17,8 @@ import GradientButton from '@/components/GradientButton'; import * as SecureStore from 'expo-secure-store'; import axios from 'axios'; import { Toast, ToastTitle, useToast } from '@gluestack-ui/themed'; +import { updateNotifications } from '@/utils/user'; +import { useTheme } from '@/components/ThemeContext'; const COLORS = { @@ -38,7 +40,9 @@ const SIZES = { }; const Notifications = () => { - let colorScheme = useColorScheme(); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; const toast = useToast(); //retrieve user settings ad assign variables accordingly const [oldInviteVal, setOldInviteVal] = useState(false); @@ -88,60 +92,21 @@ const Notifications = () => { }; const onSave = async () => { - let userEmail = await SecureStore.getItemAsync('Email'); - let authToken = await SecureStore.getItemAsync('Token'); - - try { - const response = await axios.get('https://dev.occupi.tech/api/update-notification-settings', { - params: { - email: userEmail, - invites: newInviteVal ? "on" : "off", - bookingReminder: newNotifyVal ? "on" : "off" - }, - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - withCredentials: true - }); - const data = response.data; - // console.log(`Response Data: ${JSON.stringify(data.data)}`); - console.log(data); - if (response.status === 200) { - const newSettings = { - invites: newInviteVal ? "on" : "off", - bookingReminder: newNotifyVal ? "on" : "off", - } - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - console.log(newSettings); - SecureStore.setItemAsync('Notifications', JSON.stringify(newSettings)); - router.replace('/settings'); - } else { - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - console.log(data); - } - } catch (error) { - console.error('Error:', error); - } + const settings = { + invites: newInviteVal ? "on" : "off", + bookingReminder: newNotifyVal ? "on" : "off" + }; + const response = await updateNotifications(settings) + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); + }, + }); }; const handleBack = () => { @@ -166,29 +131,29 @@ const Notifications = () => { } return ( - + - + Notifications - - Notify when someone invites me + + Notify when someone invites me { value={newInviteVal} /> - - Notify 15 minutes before booking time + + Notify 15 minutes before booking time { - const [selectedGenderIndex, setSelectedGenderIndex] = useState(1); + const [selectedGenderIndex, setSelectedGenderIndex] = useState('Male'); const [name, setName] = useState(''); const [email, setEmail] = useState(''); - const [employeeId, setEmployeeId] = useState(''); + const [employeeId, setEmployeeId] = useState('OCCUPI20242417'); const [phoneNumber, setPhoneNumber] = useState(''); const [pronouns, setPronouns] = useState(''); const [date, setDate] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isDatePickerVisible, setDatePickerVisibility] = useState(false); - let colorScheme = useColorScheme(); - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const getUserDetailsUrl= process.env.EXPO_PUBLIC_GET_USER_DETAILS; - const updateDetailsUrl = process.env.EXPO_PUBLIC_UPDATE_USER_DETAILS; - console.log(apiUrl, getUserDetailsUrl, updateDetailsUrl); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const toast = useToast(); + // console.log(apiUrl, getUserDetailsUrl, updateDetailsUrl); useEffect(() => { const getUserDetails = async () => { let result = await SecureStore.getItemAsync('UserData'); - console.log("UserData:",result); - // setUserDetails(JSON.parse(result).data); - let jsonresult = JSON.parse(result); - // console.log(jsonresult.data.details.name); - setName(String(jsonresult?.data?.details?.name)); - setEmail(String(jsonresult?.data?.email)); - setEmployeeId(String(jsonresult?.data?.occupiId)); - setPhoneNumber(String(jsonresult?.data?.details?.contactNo)); - setPronouns(String(jsonresult?.data?.details?.pronouns)); - const dateString = jsonresult?.data?.details?.dob; - const date = new Date(dateString); + console.log(result); + const email = await SecureStore.getItemAsync('Email'); + + let user = JSON.parse(result); + setName(String(user?.name)); + setEmail(String(email)); + setEmployeeId(String(user?.employeeid)); + setPhoneNumber(String(user?.number)); + setPronouns(String(user?.pronouns)); + setSelectedGenderIndex(String(user?.gender)) + const dateString = user?.dob; + console.log('dateee',dateString); + + // Manually parse the date string + const [datePart] = dateString.split('T'); + const [year, month, day] = datePart.split('-').map(Number); + + // Create a new Date object + const date = new Date(year, month, day); + // console.log(date.getDate()); // Get the day, month, and year - const day = date.getDate(); - const month = date.getMonth() + 1; // Months are zero-based - const year = date.getFullYear(); + const formattedDay = date.getDate(); + const formattedMonth = date.getMonth(); // Months are zero-based + const formattedYear = date.getFullYear(); // Format the date as MM/DD/YYYY - const formatted = `${month}/${day}/${year}`; + const formatted = `${formattedYear}-${formattedMonth}-${formattedDay}`; + // console.log(formatted); // Set the formatted date in the state - setDate(formatted) - - // console.log(JSON.parse(result).data.details.name); + setDate(formatted); }; getUserDetails(); }, []); @@ -101,81 +114,31 @@ const Profile = () => { setDatePickerVisibility(false); }; - const handleConfirm = (selectedDate) => { - setDate(selectedDate); + const handleConfirm = (selectedDate: string) => { + console.log('selected',extractDateFromTimestamp(selectedDate)); + setDate(extractDateFromTimestamp(selectedDate)); hideDatePicker(); }; - const onSave = async () => { - const body = { - "email": email, - "details": { - "contactNo": phoneNumber, - "gender": "Male", - "name": name, - "pronouns": pronouns - } - }; - // console.log(JSON.stringify(body)); - setIsLoading(true); - try { - let authToken = await SecureStore.getItemAsync('Token'); - const response = await fetch(`${apiUrl}${updateDetailsUrl}`, { - method: 'PUT', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - body: JSON.stringify(body), - credentials: "include" - }); - const data = await response.json(); - console.log(data); - if (response.ok) { - console.log(response); - setIsLoading(false); - alert('Details updated successfully'); - } else { - console.log(data); - setIsLoading(false); - } - } catch (error) { - setIsLoading(false); - console.error('Error:', error); - // setResponse('An error occurred'); - } - try { - let authToken = await SecureStore.getItemAsync('Token'); - const response = await fetch(`${apiUrl}${getUserDetailsUrl}?email=${email}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - if (response.ok) { - saveUserData(JSON.stringify(data)); - console.log(data); - } else { - console.log(data); - } - } catch (error) { - console.error('Error:', error); - } + const onSave = async () => { + const response = await updateDetails(name,date,selectedGenderIndex,phoneNumber,pronouns) + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); + }, + }); }; - async function saveUserData(value) { - await SecureStore.setItemAsync('UserData', value); - } return ( @@ -183,37 +146,37 @@ const Profile = () => { as={Feather} name={"chevron-left"} size="xl" - color={colorScheme === 'dark' ? 'white' : 'black'} + color={currentTheme === 'dark' ? 'white' : 'black'} onPress={() => router.replace('/settings')} /> - + My account - Full name + Full name - Date of birth + Date of birth - + {date} - + { onCancel={hideDatePicker} /> - Gender - {/* setSelectedGenderIndex(index)}> + Gender + setSelectedGenderIndex(index)}> - Male + Male - Female + Female - Other + Other - */} - - Email Address + + Email Address - Occupi ID + Occupi ID - Cell No + Cell No - Pronouns (optional) + Pronouns (optional) { - let colorScheme = useColorScheme(); + const colorScheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorScheme : theme; const toast = useToast(); //retrieve user settings ad assign variables accordingly @@ -60,8 +56,9 @@ const Security = () => { useEffect(() => { const getSecurityDetails = async () => { let settings = await SecureStore.getItemAsync('Security'); + // console.log(settings); const settingsObject = JSON.parse(settings); - console.log(settingsObject); + // console.log('current settings',settingsObject); if (settingsObject.mfa === "on") { setOldMfa(true); @@ -119,59 +116,22 @@ const Security = () => { const onSave = async () => { //integration here - let userEmail = await SecureStore.getItemAsync('Email'); - let authToken = await SecureStore.getItemAsync('Token'); - - try { - const response = await axios.post('https://dev.occupi.tech/api/update-security-settings', { - email: userEmail, - mfa: newMfa ? "on" : "off", - forceLogout: newForceLogout ? "on" : "off" - }, { - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - withCredentials: true - }); - const data = response.data; - // console.log(`Response Data: ${JSON.stringify(data.data)}`); - // console.log(data); - if (response.status === 200) { - const newSettings = { - mfa: newMfa ? "on" : "off", - forceLogout: newForceLogout ? "on" : "off", - } - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - // console.log(newSettings); - SecureStore.setItemAsync('Security', JSON.stringify(newSettings)); - router.replace('/settings'); - } else { - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); - } + const settings = { + mfa: newMfa ? "on" : "off", + forceLogout: newForceLogout ? "on" : "off" + }; + const response = await updateSecurity('settings', settings) + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); + }, + }); + // console.log(newSettings); }; const handleBack = () => { @@ -195,282 +155,52 @@ const Security = () => { } } return ( - - - -<<<<<<< HEAD - - - - - - Security - - - - - - - - Use faceid/touch id to enter app - - - - Use 2fa to login - - - - Force logout on app close - - - - Use 2fa to login - - - - Force logout on app close - - - handleBiometricAuth()}> - - Change Password - - Change password - - - Current Password - - { - try { - await signUpSchema.parseAsync({ - password: value, - }); - return true; - } catch (error) { - return error.message; - } - }, - }} - render={({ field: { onChange, onBlur, value } }) => ( - - - - - - - )} - /> - - - - {errors?.password?.message} - - - - - - - New Password - - { - try { - await signUpSchema.parseAsync({ - password: value, - }); - return true; - } catch (error) { - return error.message; - } - }, - }} - render={({ field: { onChange, onBlur, value } }) => ( - - - - - - - )} - /> - - - - {errors?.password?.message} - - - - - - - Confirm Password - - { - try { - await signUpSchema.parseAsync({ - password: value, - }); - - return true; - } catch (error: any) { - return error.message; - } - }, - }} - render={({ field: { onChange, onBlur, value } }) => ( - - - - - - - - )} - /> - - - - {errors?.confirmpassword?.message} - - - - - -======= + - + Security ->>>>>>> develop - - Use 2fa to login + + Use 2fa to login - - Force logout on app close + + Force logout on app close handleBiometricAuth()}> - - Change Password + + Change Password diff --git a/frontend/occupi-mobile4/screens/Settings/Settings.tsx b/frontend/occupi-mobile4/screens/Settings/Settings.tsx index ced7ac53..4b5c565e 100644 --- a/frontend/occupi-mobile4/screens/Settings/Settings.tsx +++ b/frontend/occupi-mobile4/screens/Settings/Settings.tsx @@ -8,122 +8,111 @@ import { Icon, Divider, Pressable, + Toast, + ToastTitle, Text } from '@gluestack-ui/themed'; -import { useNavigation } from '@react-navigation/native'; import { Feather, MaterialIcons } from '@expo/vector-icons'; import { router } from 'expo-router'; import Navbar from '../../components/NavBar'; import { useColorScheme } from 'react-native'; import { widthPercentageToDP as wp } from 'react-native-responsive-screen'; import * as SecureStore from 'expo-secure-store'; +import { useToast } from '@gluestack-ui/themed'; +import { UserLogout } from '@/utils/auth'; +import { useTheme } from '@/components/ThemeContext'; const Settings = () => { const [name, setName] = useState(''); const [position, setPosition] = useState(''); - const navigation = useNavigation(); - let colorScheme = useColorScheme(); + const toast = useToast(); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; useEffect(() => { const getUserDetails = async () => { let result = await SecureStore.getItemAsync('UserData'); let jsonresult = JSON.parse(result); - setName(String(jsonresult.data.details.name)); - setPosition(String(jsonresult.data.position)); + setName(String(jsonresult.name)); + setPosition(String(jsonresult.position)); }; getUserDetails(); }, []); const handleLogout = async () => { - let authToken = await SecureStore.getItemAsync('Token'); - try { - const response = await fetch('https://dev.occupi.tech/auth/logout', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - if (response.ok) { - console.log(data); - alert("logged out successfully"); - router.replace('/login'); - } else { - console.log(data); - alert("unable to logout"); + const response = await UserLogout(); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); } - } catch (error) { - console.error('Error:', error); - } + }); } - // console.log("details"+name); - - const handleNavigate = (screen) => { - navigation.navigate(screen); - }; const data = [ { title: 'My account', description: 'Make changes to your account', iconName: 'user', onPress: () => router.replace('/profile')}, { title: 'Notifications', description: 'Manage your notifications', iconName: 'bell', onPress: () => router.push('set-notifications')}, { title: 'Security', description: 'Enhance your security', iconName: 'shield', onPress: () => router.push('/set-security') }, { title: 'Appearance', description: 'Customize your viewing experience', iconName: 'image', onPress: () => router.push('/set-appearance') }, - { title: 'FAQ', description: '', iconName: 'info', onPress: () => router.push('faqpage') }, + { title: 'FAQ', description: "View the community's FAQ", iconName: 'info', onPress: () => router.push('faqpage') }, { title: 'Log out', description: 'Log out from your account', iconName: 'log-out', onPress: () => handleLogout() }, ]; const renderListItem = ({ item }) => ( - - + + - {item.title} - {item.description} + {item.title} + {item.description} - {item.accessoryRight ? item.accessoryRight() : } + {item.accessoryRight ? item.accessoryRight() : } ); return ( <> - +
- +
- {name} - {/* handleNavigate('EditProfileScreen')} /> */} + {name} + {/* handleNavigate('EditProfileScreen')} /> */} - {position} + {/* {position} */}
- - + + {data.map((item, index) => ( {renderListItem({ item })} - + ))}
- Version 0.1.0 + Version 0.1.0
@@ -191,7 +180,7 @@ const styles = StyleSheet.create({ color: '#fff', }, footerContainer: { - padding: 16, + // padding: 16, alignItems: 'center', }, versionText: { diff --git a/frontend/occupi-mobile4/services/__tests__/apiservices-test.tsx b/frontend/occupi-mobile4/services/__tests__/apiservices-test.tsx new file mode 100644 index 00000000..d760d73a --- /dev/null +++ b/frontend/occupi-mobile4/services/__tests__/apiservices-test.tsx @@ -0,0 +1,265 @@ +import axios from 'axios'; +import * as SecureStore from "expo-secure-store"; +import { + getUserDetails, + getNotificationSettings, + getUserBookings, + getSecuritySettings, + updateSecuritySettings, + updateNotificationSettings, +} from "../apiservices"; +import { NotificationSettingsReq } from '@/models/requests'; + +jest.mock('axios'); +jest.mock("expo-secure-store"); + +const mockedAxios = axios as jest.Mocked; + +describe("User API Functions", () => { + const mockEmail = "test@example.com"; + const mockAuthToken = "mockAuthToken"; + const mockSuccessResponse = { success: true, data: {} }; + const mockErrorResponse = { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + }; + + beforeEach(() => { + jest.resetAllMocks(); + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(mockAuthToken); + mockedAxios.isAxiosError.mockImplementation((payload: any) => true); + }); + + describe("getUserDetails", () => { + it("should return success response when API call is successful", async () => { + mockedAxios.get.mockResolvedValue({ data: mockSuccessResponse }); + + const result = await getUserDetails(mockEmail, mockAuthToken); + + expect(mockedAxios.get).toHaveBeenCalledWith( + "https://dev.occupi.tech/api/user-details", + expect.objectContaining({ + params: { email: mockEmail }, + headers: expect.objectContaining({ Authorization: mockAuthToken }), + }) + ); + expect(result).toEqual(mockSuccessResponse); + }); + + it("should return error response when API call fails", async () => { + mockedAxios.get.mockRejectedValue({ + response: { data: mockErrorResponse }, + }); + + const result = await getUserDetails(mockEmail, mockAuthToken); + + expect(result).toEqual(mockErrorResponse); + }); + }); + + describe("getNotificationSettings", () => { + it("should return success response when API call is successful", async () => { + (axios.get as jest.Mock).mockResolvedValue({ data: mockSuccessResponse }); + + const result = await getNotificationSettings(mockEmail); + + expect(axios.get).toHaveBeenCalledWith( + "https://dev.occupi.tech/api/get-notification-settings", + expect.objectContaining({ + params: { email: mockEmail }, + headers: expect.objectContaining({ Authorization: mockAuthToken }), + }) + ); + expect(result).toEqual(mockSuccessResponse); + }); + + it("should return error response when API call fails", async () => { + (axios.get as jest.Mock).mockRejectedValue({ + response: { data: mockErrorResponse }, + }); + + const result = await getNotificationSettings(mockEmail); + + expect(result).toEqual(mockErrorResponse); + }); + }); + + describe("getUserBookings", () => { + it("should return success response when API call is successful", async () => { + const mockAuthToken = "mockAuthToken"; + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(mockAuthToken); + (axios.get as jest.Mock).mockResolvedValue({ + data: { + data: { bookings: [] }, + status: 'success', + message: 'Bookings retrieved successfully' + } + }); + + const result = await getUserBookings(mockEmail); + + expect(axios.get).toHaveBeenCalledWith( + `https://dev.occupi.tech/api/view-bookings?filter={"email":"${mockEmail}"}`, + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: mockAuthToken }), + }) + ); + expect(result).toEqual({ + data: { bookings: [] }, + status: 'success', + message: 'Bookings retrieved successfully' + }); + }); + + it("should return error response when API call fails", async () => { + const mockAuthToken = "mockAuthToken"; + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(mockAuthToken); + (axios.get as jest.Mock).mockRejectedValue({ + response: { + data: { + data: null, + status: 'error', + message: 'Failed to retrieve bookings', + error: { + code: 'API_ERROR', + details: 'API call failed', + message: 'Failed to retrieve bookings' + } + } + }, + }); + + const result = await getUserBookings(mockEmail); + + expect(result).toEqual({ + data: null, + status: 'error', + message: 'Failed to retrieve bookings', + error: { + code: 'API_ERROR', + details: 'API call failed', + message: 'Failed to retrieve bookings' + } + }); + }); + + it("should handle case when auth token is not found", async () => { + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(null); + + const result = await getUserBookings(mockEmail); + + expect(result).toEqual({ + data: null, + status: 'error', + message: 'Authentication failed', + error: { + code: 'AUTH_ERROR', + details: 'No authentication token found', + message: 'Authentication failed' + } + }); + }); + }); + + describe("getSecuritySettings", () => { + it("should return success response when API call is successful", async () => { + (axios.get as jest.Mock).mockResolvedValue({ data: mockSuccessResponse }); + + const result = await getSecuritySettings(mockEmail); + + expect(axios.get).toHaveBeenCalledWith( + "https://dev.occupi.tech/api/get-security-settings", + expect.objectContaining({ + params: { email: mockEmail }, + headers: expect.objectContaining({ Authorization: mockAuthToken }), + }) + ); + + expect(result).toEqual(mockSuccessResponse); + }); + + it("should return error response when API call fails", async () => { + (axios.get as jest.Mock).mockRejectedValue({ + response: { data: mockErrorResponse }, + }); + + const result = await getSecuritySettings(mockEmail); + + expect(result).toEqual(mockErrorResponse); + }); + }); + + describe("updateSecuritySettings", () => { + it("should return success response when API call is successful", async () => { + (axios.post as jest.Mock).mockResolvedValue({ + data: mockSuccessResponse, + }); + const mockReq = { email: mockEmail, newSetting: "value" }; + + const result = await updateSecuritySettings(mockReq); + + expect(axios.post).toHaveBeenCalledWith( + "https://dev.occupi.tech/api/update-security-settings", + mockReq, + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: mockAuthToken }), + }) + ); + expect(result).toEqual(mockSuccessResponse); + }); + + it("should return error response when API call fails", async () => { + (axios.post as jest.Mock).mockRejectedValue({ + response: { data: mockErrorResponse }, + }); + const mockReq = { email: mockEmail, newSetting: "value" }; + + const result = await updateSecuritySettings(mockReq); + + expect(result).toEqual(mockErrorResponse); + }); + }); + + describe("updateNotificationSettings", () => { + it("should return success response when API call is successful", async () => { + (axios.get as jest.Mock).mockResolvedValue({ data: mockSuccessResponse }); + const mockReq: NotificationSettingsReq = { + email: mockEmail, + invites: "on", + bookingReminder: "on", + }; + + const result = await updateNotificationSettings(mockReq); + + expect(axios.get).toHaveBeenCalledWith( + "https://dev.occupi.tech/api/update-notification-settings", + expect.objectContaining({ + params: { req: mockReq }, + headers: expect.objectContaining({ Authorization: mockAuthToken }), + }) + ); + expect(result).toEqual(mockSuccessResponse); + }); + + it("should return error response when API call fails", async () => { + (axios.get as jest.Mock).mockRejectedValue({ + response: { data: mockErrorResponse }, + }); + const mockReq: NotificationSettingsReq = { + email: mockEmail, + invites: "on", + bookingReminder: "on", + }; + + const result = await updateNotificationSettings(mockReq); + + expect(result).toEqual(mockErrorResponse); + }); + }); +}); diff --git a/frontend/occupi-mobile4/services/__tests__/authservices-test.tsx b/frontend/occupi-mobile4/services/__tests__/authservices-test.tsx new file mode 100644 index 00000000..31c8dc3a --- /dev/null +++ b/frontend/occupi-mobile4/services/__tests__/authservices-test.tsx @@ -0,0 +1,137 @@ +import axios from 'axios'; +import * as SecureStore from 'expo-secure-store'; +import { login, logout } from '../../services/authservices'; +import { LoginReq } from '@/models/requests'; +import { LoginSuccess, Unsuccessful, Success } from '@/models/response'; + +jest.mock('axios'); +jest.mock('expo-secure-store'); + +const mockedAxios = axios as jest.Mocked; +const mockedSecureStore = SecureStore as jest.Mocked; + +describe('authservice', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockedSecureStore.getItemAsync.mockResolvedValue('mock-token'); + }); + + describe('login', () => { + const loginReq: LoginReq = { + email: 'test@example.com', + password: 'password123', + }; + + it('should return LoginSuccess on successful login', async () => { + const mockResponse: LoginSuccess = { + data: { token: 'mock-token' }, + message: 'Login successful', + status: 200, + }; + + mockedAxios.post.mockResolvedValueOnce({ data: mockResponse }); + + const result = await login(loginReq); + + expect(mockedAxios.post).toHaveBeenCalledWith( + 'https://dev.occupi.tech/auth/login-mobile', + loginReq, + expect.any(Object) + ); + expect(result).toEqual(mockResponse); + }); + + it('should throw error on failed login', async () => { + const mockError: Unsuccessful = { + data: null, + status: 'error', + message: 'Invalid credentials', + error: { + code: 'AUTH_ERROR', + details: 'Invalid email or password', + message: 'Authentication failed', + } + }; + + mockedAxios.post.mockRejectedValueOnce({ + isAxiosError: true, + response: { data: mockError } + }); + + await expect(login(loginReq)).rejects.toEqual( + expect.objectContaining({ + isAxiosError: true, + response: { data: mockError } + }) + ); + }); + + it('should throw error on network failure', async () => { + const networkError = new Error('Network error'); + mockedAxios.post.mockRejectedValueOnce(networkError); + + await expect(login(loginReq)).rejects.toThrow('Network error'); + }); + }); + + describe('logout', () => { + beforeEach(() => { + mockedSecureStore.getItemAsync.mockResolvedValue('mock-token'); + }); + + it('should return Success on successful logout', async () => { + const mockResponse: Success = { + status: 200, + message: 'Logout successful', + data: null, + }; + + mockedAxios.post.mockResolvedValueOnce({ data: mockResponse }); + + const result = await logout(); + + expect(mockedAxios.post).toHaveBeenCalledWith( + 'https://dev.occupi.tech/auth/logout', + {}, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'mock-token', + }), + }) + ); + expect(result).toEqual(mockResponse); + }); + + it('should throw error on failed logout', async () => { + const mockError: Unsuccessful = { + data: null, + status: 'error', + message: 'Logout failed', + error: { + code: 'LOGOUT_ERROR', + details: 'Unable to logout', + message: 'Logout operation failed', + } + }; + + mockedAxios.post.mockRejectedValueOnce({ + isAxiosError: true, + response: { data: mockError } + }); + + await expect(logout()).rejects.toEqual( + expect.objectContaining({ + isAxiosError: true, + response: { data: mockError } + }) + ); + }); + + it('should throw error on network failure', async () => { + const networkError = new Error('Network error'); + mockedAxios.post.mockRejectedValueOnce(networkError); + + await expect(logout()).rejects.toThrow('Network error'); + }); + }); +}); diff --git a/frontend/occupi-mobile4/services/__tests__/securestore-test.tsx b/frontend/occupi-mobile4/services/__tests__/securestore-test.tsx new file mode 100644 index 00000000..be6a6a6e --- /dev/null +++ b/frontend/occupi-mobile4/services/__tests__/securestore-test.tsx @@ -0,0 +1,83 @@ +import * as SecureStore from 'expo-secure-store'; +import { + storeUserData, + storeToken, + getUserData, + getToken, + deleteUserData, + deleteAllData, + } from '../securestore'; + + jest.mock('expo-secure-store'); + +const mockedSecureStore = SecureStore as jest.Mocked; + + describe('SecureStore', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('storeUserData', () => { + it('should store user data', async () => { + const userData = JSON.stringify({ id: 1, name: 'John Doe' }); + await storeUserData(userData); + expect(mockedSecureStore.setItemAsync).toHaveBeenCalledWith('UserData', userData); + }); + }); + + describe('storeToken', () => { + it('should store token', async () => { + const token = 'abc123'; + await storeToken(token); + expect(mockedSecureStore.setItemAsync).toHaveBeenCalledWith('Token', token); + }); + }); + + describe('getUserData', () => { + it('should return parsed user data when it exists', async () => { + const userData = { id: 1, name: 'John Doe' }; + mockedSecureStore.getItemAsync.mockResolvedValue(JSON.stringify(userData)); + const result = await getUserData(); + expect(result).toEqual(userData); + }); + + it('should return null when token does not exist', async () => { + mockedSecureStore.getItemAsync.mockResolvedValue(null); + const result = await getUserData(); // Corrected this line + expect(result).toBeNull(); + }); + }); + + describe('getToken', () => { + it('should return token when it exists', async () => { + const token = 'abc123'; + mockedSecureStore.getItemAsync.mockResolvedValue(token); + const result = await getToken(); + expect(result).toBe(token); + }); + + it('should return undefined when token does not exist', async () => { + mockedSecureStore.getItemAsync.mockResolvedValue(null); + const result = await getToken(); + expect(result).toBeUndefined(); + }); + }); + + describe('deleteUserData', () => { + it('should delete user data', async () => { + await deleteUserData(); + expect(mockedSecureStore.deleteItemAsync).toHaveBeenCalledWith('UserData'); + }); + }); + + describe('deleteAllData', () => { + it('should delete all data', async () => { + await deleteAllData(); + expect(mockedSecureStore.deleteItemAsync).toHaveBeenCalledWith('UserData'); + expect(mockedSecureStore.deleteItemAsync).toHaveBeenCalledWith('Token'); + expect(mockedSecureStore.deleteItemAsync).toHaveBeenCalledWith('Email'); + }); + }); + + + }); \ No newline at end of file diff --git a/frontend/occupi-mobile4/services/apiservices.ts b/frontend/occupi-mobile4/services/apiservices.ts new file mode 100644 index 00000000..dea22a74 --- /dev/null +++ b/frontend/occupi-mobile4/services/apiservices.ts @@ -0,0 +1,376 @@ +import { Success, Unsuccessful } from "@/models/response"; +import { SecuritySettingsReq, NotificationSettingsReq, CheckInReq, CancelBookingReq, BookRoomReq, NotificationsReq, UpdateDetailsReq } from "@/models/requests"; +// import axios from 'axios'; +import * as SecureStore from 'expo-secure-store'; +import axios, { AxiosError } from 'axios'; +import { storeUserData } from "./securestore"; + +export const getUserDetails = async (email: string, authToken: string): Promise => { + try { + const response = await axios.get("https://dev.occupi.tech/api/user-details", { + params: { email }, + headers: { Authorization: authToken }, + }); + return response.data as Success; + } catch (error) { + console.error(`Error in getUserDetails:`, error); + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + if (axiosError.response?.data) { + return axiosError.response.data; + } + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + }; + } +}; + +export async function getNotificationSettings(email: string): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + // console.log(authToken); + try { + const response = await axios.get(`https://dev.occupi.tech/api/get-notification-settings`, { + params: { + email: email + }, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `${authToken}` + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export const getUserBookings = async (email: string): Promise => { + try { + const authToken = await SecureStore.getItemAsync("Token"); + if (!authToken) { + return { + data: null, + status: 'error', + message: 'Authentication failed', + error: { + code: 'AUTH_ERROR', + details: 'No authentication token found', + message: 'Authentication failed' + } + }; + } + + const response = await axios.get( + `https://dev.occupi.tech/api/view-bookings?filter={"email":"${email}"}`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: authToken, + }, + withCredentials: true, + } + ); + // console.log(response.data); + return response.data; + } catch (error) { + console.error("Error in getUserBookings:", error); + if (axios.isAxiosError(error) && error.response) { + return error.response.data; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +}; + +export async function getNotifications(req: NotificationsReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + try { + const response = await axios.get("https://dev.occupi.tech/api/get-notifications", { + params: req, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function checkin(req: CheckInReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + console.log(req); + try { + const response = await axios.post("https://dev.occupi.tech/api/check-in", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + console.log(error.response.data) + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function updateUserDetails(req: UpdateDetailsReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + console.log(req); + try { + const response = await axios.post("https://dev.occupi.tech/api/update-user", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + storeUserData(JSON.stringify(req)); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + console.log(error.response.data) + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function bookRoom(req: BookRoomReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + console.log(req); + try { + const response = await axios.post("https://dev.occupi.tech/api/book-room", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + console.log(error.response.data) + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function cancelBooking(req: CancelBookingReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + console.log(req); + try { + const response = await axios.post("https://dev.occupi.tech/api/cancel-booking", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + console.log(error.response.data) + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function getSecuritySettings(email: string): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + // console.log(authToken); + try { + const response = await axios.get(`https://dev.occupi.tech/api/get-security-settings`, { + params: { + email: email + }, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `${authToken}` + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function updateSecuritySettings(req: SecuritySettingsReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + try { + const response = await axios.post("https://dev.occupi.tech/api/update-security-settings", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function updateNotificationSettings(req: NotificationSettingsReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + try { + const response = await axios.get("https://dev.occupi.tech/api/update-notification-settings", { + params: { + req + }, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/services/authservices.ts b/frontend/occupi-mobile4/services/authservices.ts new file mode 100644 index 00000000..06ed7ed6 --- /dev/null +++ b/frontend/occupi-mobile4/services/authservices.ts @@ -0,0 +1,123 @@ +import { LoginReq, RegisterReq, VerifyOTPReq } from "@/models/requests"; +import { LoginSuccess, Unsuccessful, Success } from "@/models/response"; +import axios from 'axios'; +import dotenv from 'dotenv'; +import * as SecureStore from 'expo-secure-store'; + +// dotenv.config(); +// const devUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; +// console.log(devUrl); + +export async function login(req: LoginReq): Promise { + try { + const response = await axios.post("https://dev.occupi.tech/auth/login-mobile", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as LoginSuccess; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + // console.log(error.response.data); + return error.response.data as Unsuccessful; + } else { + throw error; + } + } +} + +export async function register(req: RegisterReq): Promise { + console.log(req); + try { + const response = await axios.post("https://dev.occupi.tech/auth/register", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + withCredentials: true + }); + console.log(response.data); + return response.data as Success; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + console.log(error.response.data); + return error.response.data as Unsuccessful; + } else { + throw error; + } + } +} + +export async function verifyOtpRegister(req: VerifyOTPReq): Promise { + try { + const response = await axios.post("https://dev.occupi.tech/auth/verify-otp", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as LoginSuccess; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + // console.log(error.response.data); + return error.response.data as Unsuccessful; + } else { + throw error; + } + } +} + +export async function verifyOtplogin(req: VerifyOTPReq): Promise { + try { + const response = await axios.post("https://dev.occupi.tech/auth/verify-otp-mobile-login", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as LoginSuccess; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + // console.log(error.response.data); + return error.response.data as Unsuccessful; + } else { + throw error; + } + } +} + +export async function logout(): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + // console.log('token',authToken); + try { + const response = await axios.post("https://dev.occupi.tech/auth/logout", {},{ + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `${authToken}` + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as Success; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + // console.log(error.response.data); + return error.response.data as Unsuccessful; + } else { + throw error; + } + } +} + +// login({ +// email: "boygenius31115@gmail.com", +// password: "Qwert@123" +// }) \ No newline at end of file diff --git a/frontend/occupi-mobile4/services/securestore.ts b/frontend/occupi-mobile4/services/securestore.ts new file mode 100644 index 00000000..2f34575c --- /dev/null +++ b/frontend/occupi-mobile4/services/securestore.ts @@ -0,0 +1,82 @@ +import * as SecureStore from 'expo-secure-store'; +import { User } from '@/models/data'; + +export async function storeUserData(value: string) { + await SecureStore.setItemAsync('UserData', value); +} + +export async function storeToken(value: string) { + await SecureStore.setItemAsync('Token', value); +} + +export async function storeUserEmail(value: string) { + await SecureStore.setItemAsync('Email', value); +} + +export async function setState(value: string) { + await SecureStore.setItemAsync('AppState', value); +} + +export async function storeNotificationSettings(value: string) { + await SecureStore.setItemAsync('Notifications', value); +} + +export async function storeTheme(value: string) { + await SecureStore.setItemAsync('Theme', value); +} + +export async function storeAccentColour(value: string) { + await SecureStore.setItemAsync('accentColour', value); +} + +export async function storeSecuritySettings(value: string) { + await SecureStore.setItemAsync('Security', value); +} + +export async function getUserData() { + let result: string | null = await SecureStore.getItemAsync('UserData'); + return result ? JSON.parse(result) : null; +} + +export async function getToken() { + let result = await SecureStore.getItemAsync('Token'); + const tokenVal = result; + // console.log('token', result); + return tokenVal || undefined; +} + +export async function getUserEmail() { + let result = await SecureStore.getItemAsync('Email'); + return result; +} + +export async function getCurrentRoom() { + let result = await SecureStore.getItemAsync('CurrentRoom'); + return result; +} + +export async function deleteUserData() { + await SecureStore.deleteItemAsync('UserData'); +} + +export async function deleteToken() { + await SecureStore.deleteItemAsync('Token'); +} + +export async function deleteUserEmail() { + await SecureStore.deleteItemAsync('Email'); +} + +export async function deleteNotificationSettings() { + await SecureStore.deleteItemAsync('Notifications'); +} + +export async function deleteSecuritySettings() { + await SecureStore.deleteItemAsync('Security'); +} + +export async function deleteAllData() { + await SecureStore.deleteItemAsync('UserData'); + await SecureStore.deleteItemAsync('Token'); + await SecureStore.deleteItemAsync('Email'); +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/__tests__/auth-test.tsx b/frontend/occupi-mobile4/utils/__tests__/auth-test.tsx new file mode 100644 index 00000000..0e3721c5 --- /dev/null +++ b/frontend/occupi-mobile4/utils/__tests__/auth-test.tsx @@ -0,0 +1,134 @@ +import { UserLogin, UserLogout } from '../auth'; +import { login, logout } from "../../services/authservices"; +import { fetchNotificationSettings, fetchSecuritySettings, fetchUserDetails } from "../user"; +import { router } from 'expo-router'; +import { storeUserEmail, storeToken, setState, deleteToken, deleteUserData, deleteUserEmail, deleteNotificationSettings, deleteSecuritySettings } from "../../services/securestore"; + +// Mock dependencies +jest.mock('../../services/authservices'); +jest.mock('../user'); +jest.mock('expo-router', () => ({ + router: { + replace: jest.fn(), + }, +})); +jest.mock('../../services/securestore'); + +describe('auth.ts', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('UserLogin', () => { + it('should login successfully and set up user data', async () => { + const mockEmail = 'test@example.com'; + const mockPassword = 'password123'; + const mockToken = 'mock-token'; + + (login as jest.Mock).mockResolvedValue({ + status: 200, + data: { token: mockToken }, + message: 'Login successful' + }); + + const result = await UserLogin(mockEmail, mockPassword); + + expect(storeUserEmail).toHaveBeenCalledWith(mockEmail); + expect(login).toHaveBeenCalledWith({ email: mockEmail, password: mockPassword }); + expect(setState).toHaveBeenCalledWith('logged_in'); + expect(storeToken).toHaveBeenCalledWith(mockToken); + expect(fetchUserDetails).toHaveBeenCalledWith(mockEmail, mockToken); + expect(fetchNotificationSettings).toHaveBeenCalledWith(mockEmail); + expect(fetchSecuritySettings).toHaveBeenCalledWith(mockEmail); + expect(router.replace).toHaveBeenCalledWith('/home'); + expect(result).toBe('Login successful'); + }); + + it('should handle login failure', async () => { + const mockEmail = 'test@example.com'; + const mockPassword = 'wrong-password'; + + (login as jest.Mock).mockResolvedValue({ + status: 401, + message: 'Invalid credentials' + }); + + const result = await UserLogin(mockEmail, mockPassword); + + expect(storeUserEmail).toHaveBeenCalledWith(mockEmail); + expect(login).toHaveBeenCalledWith({ email: mockEmail, password: mockPassword }); + expect(setState).not.toHaveBeenCalled(); + expect(storeToken).not.toHaveBeenCalled(); + expect(fetchUserDetails).not.toHaveBeenCalled(); + expect(fetchNotificationSettings).not.toHaveBeenCalled(); + expect(fetchSecuritySettings).not.toHaveBeenCalled(); + expect(router.replace).not.toHaveBeenCalled(); + expect(result).toBe('Invalid credentials'); + }); + + it('should handle errors during login', async () => { + const mockEmail = 'test@example.com'; + const mockPassword = 'password123'; + + (login as jest.Mock).mockRejectedValue(new Error('Network error')); + + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await UserLogin(mockEmail, mockPassword); + + expect(consoleSpy).toHaveBeenCalledWith('Error:', expect.any(Error)); + consoleSpy.mockRestore(); + }); + }); + + describe('UserLogout', () => { + it('should logout successfully and clear user data', async () => { + (logout as jest.Mock).mockResolvedValue({ + status: 200, + message: 'Logout successful' + }); + + const result = await UserLogout(); + + expect(logout).toHaveBeenCalled(); + expect(setState).toHaveBeenCalledWith('logged_out'); + expect(deleteNotificationSettings).toHaveBeenCalled(); + expect(deleteSecuritySettings).toHaveBeenCalled(); + expect(deleteUserData).toHaveBeenCalled(); + expect(deleteToken).toHaveBeenCalled(); + expect(deleteUserEmail).toHaveBeenCalled(); + expect(router.replace).toHaveBeenCalledWith('/login'); + expect(result).toBe('Logout successful'); + }); + + it('should handle logout failure', async () => { + (logout as jest.Mock).mockResolvedValue({ + status: 400, + message: 'Logout failed' + }); + + const result = await UserLogout(); + + expect(logout).toHaveBeenCalled(); + expect(setState).not.toHaveBeenCalled(); + expect(deleteNotificationSettings).not.toHaveBeenCalled(); + expect(deleteSecuritySettings).not.toHaveBeenCalled(); + expect(deleteUserData).not.toHaveBeenCalled(); + expect(deleteToken).not.toHaveBeenCalled(); + expect(deleteUserEmail).not.toHaveBeenCalled(); + expect(router.replace).not.toHaveBeenCalled(); + expect(result).toBe('Logout failed'); + }); + + it('should handle errors during logout', async () => { + (logout as jest.Mock).mockRejectedValue(new Error('Network error')); + + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await UserLogout(); + + expect(consoleSpy).toHaveBeenCalledWith('Error:', expect.any(Error)); + consoleSpy.mockRestore(); + }); + }); +}); \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/__tests__/bookings-test.tsx b/frontend/occupi-mobile4/utils/__tests__/bookings-test.tsx new file mode 100644 index 00000000..b59f3e88 --- /dev/null +++ b/frontend/occupi-mobile4/utils/__tests__/bookings-test.tsx @@ -0,0 +1,125 @@ +import { fetchUserBookings, userCheckin, userCancelBooking } from '../bookings'; +import { getUserBookings, checkin, cancelBooking } from '../../services/apiservices'; +import * as SecureStore from 'expo-secure-store'; +import { router } from 'expo-router'; + +// Mock dependencies +jest.mock('../../services/apiservices'); +jest.mock('expo-secure-store'); +jest.mock('expo-router', () => ({ + router: { + replace: jest.fn(), + }, +})); + +beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('../bookings.ts', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('fetchUserBookings', () => { + it('should fetch user bookings successfully', async () => { + const mockEmail = 'test@example.com'; + const mockBookings = [{ id: 1, title: 'Booking 1' }, { id: 2, title: 'Booking 2' }]; + + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(mockEmail); + (getUserBookings as jest.Mock).mockResolvedValue({ status: 200, data: mockBookings }); + + const result = await fetchUserBookings(); + + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('Email'); + expect(getUserBookings).toHaveBeenCalledWith(mockEmail); + expect(result).toEqual(mockBookings); + }); + + it('should handle errors when fetching user bookings', async () => { + const mockError = new Error('API Error'); + + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue('test@example.com'); + (getUserBookings as jest.Mock).mockRejectedValue(mockError); + + await expect(fetchUserBookings()).rejects.toThrow('API Error'); + }); + }); + + describe('userCheckin', () => { + it('should perform user check-in successfully', async () => { + const mockRoom = { occupiId: 'room123' }; + const mockEmail = 'test@example.com'; + + (SecureStore.getItemAsync as jest.Mock) + .mockResolvedValueOnce(JSON.stringify(mockRoom)) + .mockResolvedValueOnce(mockEmail); + (checkin as jest.Mock).mockResolvedValue({ status: 200, message: 'Check-in successful' }); + + const result = await userCheckin(); + + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('CurrentRoom'); + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('Email'); + expect(checkin).toHaveBeenCalledWith({ email: mockEmail, bookingId: 'room123' }); + expect(result).toBe('Check-in successful'); + }); + + it('should handle errors during user check-in', async () => { + const mockError = new Error('Check-in Error'); + + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue('{}'); + (checkin as jest.Mock).mockRejectedValue(mockError); + + await expect(userCheckin()).rejects.toThrow('Check-in Error'); + }); + }); + + describe('userCancelBooking', () => { + it('should cancel user booking successfully', async () => { + const mockRoom = { + occupiId: 'booking123', + emails: ['user1@example.com', 'user2@example.com'], + roomId: 'room123', + creator: 'user1@example.com', + date: '2023-07-30', + start: '10:00', + end: '11:00', + floorNo: 1, + roomName: 'Meeting Room A' + }; + + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(JSON.stringify(mockRoom)); + (cancelBooking as jest.Mock).mockResolvedValue({ status: 200, message: 'Booking cancelled successfully' }); + + const result = await userCancelBooking(); + + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('CurrentRoom'); + expect(cancelBooking).toHaveBeenCalledWith(expect.objectContaining({ + bookingId: 'booking123', + emails: ['user1@example.com', 'user2@example.com'], + roomId: 'room123', + creator: 'user1@example.com', + date: '2023-07-30', + start: '10:00', + end: '11:00', + floorNo: 1, + roomName: 'Meeting Room A' + })); + expect(router.replace).toHaveBeenCalledWith('/home'); + expect(result).toBe('Booking cancelled successfully'); + }); + + it('should handle errors during booking cancellation', async () => { + const mockError = new Error('Cancellation Error'); + + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue('{}'); + (cancelBooking as jest.Mock).mockRejectedValue(mockError); + + await expect(userCancelBooking()).rejects.toThrow('Cancellation Error'); + }); + }); +}); \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/__tests__/notifications-test.tsx b/frontend/occupi-mobile4/utils/__tests__/notifications-test.tsx new file mode 100644 index 00000000..af726b8f --- /dev/null +++ b/frontend/occupi-mobile4/utils/__tests__/notifications-test.tsx @@ -0,0 +1,90 @@ +import * as Notifications from 'expo-notifications'; +import { sendPushNotification } from '../notifications'; // Adjust the import path as needed + +jest.mock('expo-notifications'); +jest.mock('node-fetch'); + +describe('Notification Functions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('setNotificationHandler sets the correct handler', () => { + // Manually call setNotificationHandler to ensure it's executed + Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: false, + shouldSetBadge: false, + }), + }); + + expect(Notifications.setNotificationHandler).toHaveBeenCalledWith(expect.objectContaining({ + handleNotification: expect.any(Function), + })); + }); + + test('handleNotification returns correct configuration', async () => { + // Manually call setNotificationHandler to ensure it's executed + Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: false, + shouldSetBadge: false, + }), + }); + + const handler = (Notifications.setNotificationHandler as jest.Mock).mock.calls[0][0]; + const result = await handler.handleNotification(); + expect(result).toEqual({ + shouldShowAlert: true, + shouldPlaySound: false, + shouldSetBadge: false, + }); + }); + + test('sendPushNotification sends notifications to all tokens', async () => { + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ data: 'success' }), + }); + global.fetch = mockFetch as any; + + const expoPushTokens = ['token1', 'token2', 'token3']; + const title = 'Test Title'; + const body = 'Test Body'; + + await sendPushNotification(expoPushTokens, title, body); + + expect(mockFetch).toHaveBeenCalledTimes(3); + + expoPushTokens.forEach((token, index) => { + expect(mockFetch).toHaveBeenNthCalledWith(index + 1, 'https://exp.host/--/api/v2/push/send', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Accept-encoding': 'gzip, deflate', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + to: token, + sound: 'default', + title: title, + body: body, + data: { someData: 'goes here' }, + }), + }); + }); + }); + + test('sendPushNotification handles errors', async () => { + const mockFetch = jest.fn().mockRejectedValue(new Error('Network error')); + global.fetch = mockFetch as any; + + const expoPushTokens = ['token1']; + const title = 'Test Title'; + const body = 'Test Body'; + + await expect(sendPushNotification(expoPushTokens, title, body)).rejects.toThrow('Network error'); + }); +}); \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/__tests__/user-test.tsx b/frontend/occupi-mobile4/utils/__tests__/user-test.tsx new file mode 100644 index 00000000..8c4d81ab --- /dev/null +++ b/frontend/occupi-mobile4/utils/__tests__/user-test.tsx @@ -0,0 +1,127 @@ +import * as SecureStore from 'expo-secure-store'; +import { + storeUserData, + storeToken, + storeUserEmail, + setState, + storeNotificationSettings, + storeSecuritySettings, + getUserData, + getToken, + getUserEmail, + getCurrentRoom, + deleteUserData, + deleteToken, + deleteUserEmail, + deleteNotificationSettings, + deleteSecuritySettings, + deleteAllData +} from '../../services/securestore'; + +jest.mock('expo-secure-store'); + +describe('Secure Store Functions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('storeUserData stores user data', async () => { + const userData = JSON.stringify({ name: 'John Doe', email: 'john@example.com' }); + await storeUserData(userData); + expect(SecureStore.setItemAsync).toHaveBeenCalledWith('UserData', userData); + }); + + test('storeToken stores token', async () => { + const token = 'abc123'; + await storeToken(token); + expect(SecureStore.setItemAsync).toHaveBeenCalledWith('Token', token); + }); + + test('storeUserEmail stores email', async () => { + const email = 'john@example.com'; + await storeUserEmail(email); + expect(SecureStore.setItemAsync).toHaveBeenCalledWith('Email', email); + }); + + test('setState stores app state', async () => { + const state = 'active'; + await setState(state); + expect(SecureStore.setItemAsync).toHaveBeenCalledWith('AppState', state); + }); + + test('storeNotificationSettings stores notification settings', async () => { + const settings = JSON.stringify({ pushEnabled: true }); + await storeNotificationSettings(settings); + expect(SecureStore.setItemAsync).toHaveBeenCalledWith('Notifications', settings); + }); + + test('storeSecuritySettings stores security settings', async () => { + const settings = JSON.stringify({ twoFactor: true }); + await storeSecuritySettings(settings); + expect(SecureStore.setItemAsync).toHaveBeenCalledWith('Security', settings); + }); + + test('getUserData retrieves user data', async () => { + const userData = { name: 'John Doe', email: 'john@example.com' }; + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(JSON.stringify(userData)); + const result = await getUserData(); + expect(result).toEqual(userData); + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('UserData'); + }); + + test('getToken retrieves token', async () => { + const token = 'abc123'; + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(token); + const result = await getToken(); + expect(result).toBe(token); + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('Token'); + }); + + test('getUserEmail retrieves email', async () => { + const email = 'john@example.com'; + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(email); + const result = await getUserEmail(); + expect(result).toBe(email); + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('Email'); + }); + + test('getCurrentRoom retrieves current room', async () => { + const room = 'living-room'; + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(room); + const result = await getCurrentRoom(); + expect(result).toBe(room); + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('CurrentRoom'); + }); + + test('deleteUserData deletes user data', async () => { + await deleteUserData(); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('UserData'); + }); + + test('deleteToken deletes token', async () => { + await deleteToken(); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('Token'); + }); + + test('deleteUserEmail deletes email', async () => { + await deleteUserEmail(); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('Email'); + }); + + test('deleteNotificationSettings deletes notification settings', async () => { + await deleteNotificationSettings(); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('Notifications'); + }); + + test('deleteSecuritySettings deletes security settings', async () => { + await deleteSecuritySettings(); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('Security'); + }); + + test('deleteAllData deletes all data', async () => { + await deleteAllData(); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('UserData'); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('Token'); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('Email'); + }); +}); \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/auth.ts b/frontend/occupi-mobile4/utils/auth.ts new file mode 100644 index 00000000..603a04ae --- /dev/null +++ b/frontend/occupi-mobile4/utils/auth.ts @@ -0,0 +1,144 @@ +//this folder contains functions that will call the service functions which make api requests for authentication +//the purpose of this file is to refine and process the data and return these to the View + +import { login, logout, register, verifyOtplogin, verifyOtpRegister } from "../services/authservices"; +import { fetchNotificationSettings, fetchSecuritySettings, fetchUserDetails } from "./user"; +import { router } from 'expo-router'; +import { storeUserEmail, storeToken, setState, deleteToken, deleteUserData, deleteUserEmail, deleteNotificationSettings, deleteSecuritySettings } from "../services/securestore"; +import { retrievePushToken } from "./notifications"; + + +export async function UserLogin(email: string, password: string) { + storeUserEmail(email); + try { + const response = await login({ + email: email, + password: password + }); + if (response.status === 200) { + console.log('responseee',response); + if (response.data !== null) { + setState('logged_in'); + storeToken(response.data.token); + console.log('here'); + fetchUserDetails(email, response.data.token); + fetchNotificationSettings(email); + fetchSecuritySettings(email); + router.replace('/home'); + } + else { + setState('verify_otp_login'); + router.replace('verify-otp') + } + + return response.message; + } + else { + console.log('woahhh', response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function userRegister(email: string, password: string, employeeId: string) { + let expoPushToken = await retrievePushToken(); + storeUserEmail(email); + try { + const response = await register({ + email: email, + password: password, + // employee_id: employeeId, + expoPushToken: expoPushToken + }); + if (response.status === 200) { + console.log('responseee',response); + setState('verify_otp_register'); + router.replace('/verify-otp'); + return response.message; + } + else { + console.log('woahhh', response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function verifyUserOtpRegister(email: string, otp: string) { + try { + const response = await verifyOtpRegister({ + email: email, + otp: otp + }); + if (response.status === 200) { + console.log('responseee',response); + router.replace('/set-details'); + router.replace('/login'); + return response.message; + } + else { + console.log('woahhh', response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function VerifyUserOtpLogin(email : string, otp : string) { + try { + const response = await verifyOtplogin({ + email: email, + otp: otp + }); + if (response.status === 200) { + console.log('responseee',response); + if (response.data !== null) { + setState('logged_in'); + storeToken(response.data.token); + console.log('here'); + fetchUserDetails(email, response.data.token); + fetchNotificationSettings(email); + fetchSecuritySettings(email); + router.replace('/home'); + } + + return response.message; + } + else { + console.log('woahhh', response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function UserLogout() { + // console.log('hhhh'); + try { + const response = await logout(); + if (response.status === 200) { + // console.log('responseee',response); + setState('logged_out'); + deleteNotificationSettings(); + deleteSecuritySettings(); + deleteUserData(); + deleteToken(); + deleteUserEmail(); + router.replace('/login'); + return response.message; + } + else { + console.log('woahhh', response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +// UserLogin("kamogelomoeketse@gmail.com", "Qwerty@123"); //test \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/bookings.ts b/frontend/occupi-mobile4/utils/bookings.ts new file mode 100644 index 00000000..91cfede8 --- /dev/null +++ b/frontend/occupi-mobile4/utils/bookings.ts @@ -0,0 +1,101 @@ +import { Booking, Room } from "@/models/data"; +import { bookRoom, cancelBooking, checkin, getUserBookings } from "../services/apiservices"; +import * as SecureStore from 'expo-secure-store'; +import { router } from 'expo-router'; +import { BookRoomReq, CancelBookingReq } from "@/models/requests"; + +export async function fetchUserBookings(): Promise { + let email = await SecureStore.getItemAsync('Email'); + try { + const response = await getUserBookings(email); + if (response.status === 200) { + // console.log('response', response.data); + return response.data; + // console.log(settings); + } + else { + console.log(response) + } + return response.data as Booking[]; + } catch (error) { + console.error('Error:', error); + throw error; // Add a throw statement to handle the error case + } +} + +export async function userBookRoom(attendees : string[], startTime : string, endTime : string) { + let roomstring = await SecureStore.getItemAsync("BookingInfo"); + let email = await SecureStore.getItemAsync("Email"); + const room : Booking = JSON.parse(roomstring as string); + const body : BookRoomReq = { + roomName: room.roomName, + creator: email, + date: room.date+"T00:00:00.000+00:00", + start: room.date+"T"+startTime+":00.000+00:00", + end: room.date+"T"+endTime+":00.000+00:00", + floorNo: room.floorNo, + emails: attendees, + roomId: room.roomId + + } + console.log(body); + try { + const response = await bookRoom(body); + if (response.status === 200) { + return response.message; + } + return response.message; + } catch (error) { + console.error('Error:', error); + throw error; + } +} + +export async function userCheckin() { + let roomstring = await SecureStore.getItemAsync("CurrentRoom"); + const room = JSON.parse(roomstring as string); + const bookingId = room?.occupiId; + let email = await SecureStore.getItemAsync('Email'); + const body = { + email: email as string, + bookingId: bookingId + } + try { + const response = await checkin(body); + if (response.status === 200) { + return response.message; + } + return response.message; + } catch (error) { + console.error('Error:', error); + throw error; + } +} + +export async function userCancelBooking() { + let roomstring = await SecureStore.getItemAsync("CurrentRoom"); + const room : Booking = JSON.parse(roomstring as string); + let email = await SecureStore.getItemAsync('Email'); + const body : CancelBookingReq = { + bookingId: room?.occupiId, + emails: room?.emails, + roomId: room?.roomId, + creator: room.creator, + date: room?.date, + start: room?.start, + end: room?.end, + floorNo: room?.floorNo, + roomName: room?.roomName + } + try { + const response = await cancelBooking(body); + if (response.status === 200) { + router.replace('/home'); + return response.message; + } + return response.message; + } catch (error) { + console.error('Error:', error); + throw error; + } +} diff --git a/frontend/occupi-mobile4/utils/dashboard.ts b/frontend/occupi-mobile4/utils/dashboard.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/occupi-mobile4/utils/notifications.ts b/frontend/occupi-mobile4/utils/notifications.ts new file mode 100644 index 00000000..f0d1ff7d --- /dev/null +++ b/frontend/occupi-mobile4/utils/notifications.ts @@ -0,0 +1,117 @@ +import * as Device from 'expo-device'; +import { Platform } from 'react-native'; +import * as Notifications from 'expo-notifications'; +import Constants from 'expo-constants'; +import * as SecureStore from 'expo-secure-store'; +import { NotificationsReq } from '@/models/requests'; +import { getNotifications } from '@/services/apiservices'; + +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: false, + shouldSetBadge: false, + }), +}); + +export async function retrievePushToken(): Promise { + const token = await registerForPushNotificationsAsync(); + // console.log(token); + return token as string; +} + +// retrievePushToken(); +// console.log('yurp'); + +async function registerForPushNotificationsAsync() { + let token; + + if (Platform.OS === 'android') { + await Notifications.setNotificationChannelAsync('default', { + name: 'default', + importance: Notifications.AndroidImportance.MAX, + vibrationPattern: [0, 250, 250, 250], + lightColor: '#FF231F7C', + }); + } + + if (Device.isDevice) { + const { status: existingStatus } = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + if (finalStatus !== 'granted') { + alert('Failed to get push token for push notification!'); + return; + } + // Learn more about projectId: + // https://docs.expo.dev/push-notifications/push-notifications-setup/#configure-projectid + // EAS projectId is used here. + try { + const projectId = + Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId; + if (!projectId) { + throw new Error('Project ID not found'); + } + token = ( + await Notifications.getExpoPushTokenAsync({ + projectId, + }) + ).data; + // console.log(token); + } catch (e) { + token = `${e}`; + } + } else { + alert('Must use physical device for Push Notifications'); + } + + return token; +} + + export async function sendPushNotification(expoPushTokens: string[], title: string, body: string) { + const messages = expoPushTokens.map(token => ({ + to: token, + sound: 'default', + title: title, + body: body, + data: { someData: 'goes here' }, + })); + + for (const message of messages) { + await fetch('https://exp.host/--/api/v2/push/send', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Accept-encoding': 'gzip, deflate', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(message), + }); + } +} + +export async function getUserNotifications() { + let email = await SecureStore.getItemAsync('Email'); + + try { + const request : NotificationsReq = { + filter: { + emails: [email] + } + }; + const response = await getNotifications(request); + if (response.status === 200) { + // console.log('notifications', response.data); + return response.data + } + else { + console.log(response) + return response.data; + } + } catch (error) { + console.error('Error:', error); + } +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/occupancy.ts b/frontend/occupi-mobile4/utils/occupancy.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/occupi-mobile4/utils/settings.ts b/frontend/occupi-mobile4/utils/settings.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/occupi-mobile4/utils/user.ts b/frontend/occupi-mobile4/utils/user.ts new file mode 100644 index 00000000..8b3e70b2 --- /dev/null +++ b/frontend/occupi-mobile4/utils/user.ts @@ -0,0 +1,182 @@ +import { UpdateDetailsReq } from "@/models/requests"; +import { getUserDetails, getNotificationSettings, getSecuritySettings, updateSecuritySettings, updateNotificationSettings, updateUserDetails } from "../services/apiservices"; +import { storeUserData, storeNotificationSettings, getUserData, storeSecuritySettings, setState } from "../services/securestore"; +import { router } from 'expo-router'; +import * as SecureStore from 'expo-secure-store'; + + +export async function fetchUserDetails(email: string, token: string) { + try { + const response = await getUserDetails(email, token); + if (response.status === 200) { + storeUserData(JSON.stringify(response.data)); + } + else { + console.log(response) + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function fetchNotificationSettings(email: string) { + try { + const response = await getNotificationSettings(email); + if (response.status === 200) { + const settings = { + invites: response.data.invites, + bookingReminder: response.data.bookingReminder + }; + // console.log('settings response', response.data); + // console.log(settings); + storeNotificationSettings(JSON.stringify(settings)); + } + else { + console.log(response) + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function fetchSecuritySettings(email: string) { + try { + const response = await getSecuritySettings(email); + if (response.status === 200) { + const settings = { + mfa: response.data.mfa, + forcelogout: response.data.forceLogout + }; + // console.log('settings response', response.data); + // console.log(settings); + storeSecuritySettings(JSON.stringify(settings)); + } + else { + console.log(response) + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function updateSecurity(type: string, values: any) { + let userData = await SecureStore.getItemAsync('UserData'); + let user = JSON.parse(userData || ""); + let email = user.email; + if (type === "settings") { + try { + const request = { + email: email, + mfa: values.mfa, + forceLogout: values.forceLogout + } + const response = await updateSecuritySettings(request); + if (response.status === 200) { + const settings = { + mfa: values.mfa, + forceLogout: values.forceLogout + }; + console.log('settings response', response); + console.log(settings); + storeSecuritySettings(JSON.stringify(settings)); + router.replace('/settings') + return "Settings updated successfully" + } + else { + console.log(response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } + } else { + try { + const request = { + email: email, + currentPassword: values.currentPassword, + newPassword: values.newPassword, + newPasswordConfirm: values.newPasswordConfirm + } + const response = await updateSecuritySettings(request); + if (response.status === 200) { + router.replace('/set-security') + return "Successfully changed password" + } + else { + console.log(response); + return response.message; + } + } catch (error) { + console.error('Error:', error); + } + } +} + +export async function updateDetails(name: string, dob: string, gender: string, cellno: string, pronouns: string) { + const email = await SecureStore.getItemAsync('Email'); + const state = await SecureStore.getItemAsync('AppState'); + try { + const request : UpdateDetailsReq = { + session_email: email, + name: name, + dob: dob + "T00:00:00.000Z", + gender: gender, + number: cellno, + pronouns: pronouns, + } + const response = await updateUserDetails(request); + if (response.status === 200) { + console.log(response); + if (state === "verify_otp_register") { + setState("logged_out"); + router.replace('login'); + } + router.replace('/settings') + return "Details updated successfully" + } + else { + console.log(response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function updateNotifications(values: any) { + let userData = await SecureStore.getItemAsync('UserData'); + let user = JSON.parse(userData || ""); + let email = user.email; + try { + const request = { + email: email, + invites: values.mfa, + bookingReminder: values.forceLogout + } + const response = await updateNotificationSettings(request); + if (response.status === 200) { + const settings = { + invites: values.invites, + bookingReminder: values.bookingReminder + }; + console.log('settings response', response); + console.log(settings); + storeNotificationSettings(JSON.stringify(settings)); + router.replace('/settings') + return "Settings updated successfully" + } + else { + console.log(response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function fetchUsername() { + let userData = await SecureStore.getItemAsync('UserData'); + let user = JSON.parse(userData || "{}"); + // console.log(user.name); + return user.name; +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/utils.ts b/frontend/occupi-mobile4/utils/utils.ts index 83af1778..d6ae0fbb 100644 --- a/frontend/occupi-mobile4/utils/utils.ts +++ b/frontend/occupi-mobile4/utils/utils.ts @@ -1,12 +1,33 @@ import * as SecureStore from 'expo-secure-store'; + + export const getAccentColour = async () => { let accentcolour = await SecureStore.getItemAsync('accentColour'); if (!accentcolour) { return "greenyellow"; } - else - { - return accentcolour; + else { + return accentcolour; + } +}; + +export const getTheme = async () => { + let theme = await SecureStore.getItemAsync('Theme'); + if (!theme) { + return "dark"; } - }; \ No newline at end of file + else { + return theme; + } +}; + +export const theme = getTheme(); + +export function extractDateFromTimestamp(timestamp: string): string { + const date = new Date(timestamp); + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); + const day = String(date.getUTCDate()+1).padStart(2, '0'); + return `${year}-${month}-${day}`; + } \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/viewbookings.ts b/frontend/occupi-mobile4/utils/viewbookings.ts new file mode 100644 index 00000000..e69de29b