From bf147af3efa9b9f0d050823e40f975bfb0b1fc81 Mon Sep 17 00:00:00 2001 From: c-d-p Date: Tue, 22 Apr 2025 00:10:57 +0200 Subject: [PATCH] Added admin page --- .../admin/__pycache__/api.cpython-312.pyc | Bin 2230 -> 2506 bytes backend/modules/admin/api.py | 25 +++-- .../auth/__pycache__/api.cpython-312.pyc | Bin 4396 -> 4396 bytes interfaces/nativeapp/src/api/admin.ts | 16 +++ interfaces/nativeapp/src/api/client.ts | 4 +- .../nativeapp/src/components/WebSidebar.tsx | 31 ++++-- .../nativeapp/src/contexts/AuthContext.tsx | 97 +++++++++++++----- .../src/navigation/MobileTabNavigator.tsx | 18 +++- .../src/navigation/WebContentNavigator.tsx | 10 +- .../nativeapp/src/screens/AdminScreen.tsx | 81 +++++++++++++++ interfaces/nativeapp/src/types/navigation.ts | 2 + 11 files changed, 241 insertions(+), 43 deletions(-) create mode 100644 interfaces/nativeapp/src/api/admin.ts create mode 100644 interfaces/nativeapp/src/screens/AdminScreen.tsx diff --git a/backend/modules/admin/__pycache__/api.cpython-312.pyc b/backend/modules/admin/__pycache__/api.cpython-312.pyc index 275f34eaac981785b5c31b0bf57d7c7ba2d6b4c3..6f2b3a92d54ea3c67b71415359b4215f09ba09c9 100644 GIT binary patch literal 2506 zcmb7GO>7fK6rNqL*XzGHP6#C7Z;J@v5NyOxTcLoI5S0QAs=}YTQngxpCW(_>ubJIQ zI7%c?sQ?KHI3);G1#uu19NJ^mOF2}k)Qh7~5k?@8+8%l{gcGN}8G9k*0v*XS?{D6` zdGF1){v(-;AsD|sP)9GS2>r#GU_=|lk}e@MgD}F9jU364Bu>k=?1YSvqZo=4Ho`&{ zvQ;NyLWy_XX~k(Gk*J6y5y*$-4lo^7aLWbdjkh(G*37DZxr>a%qJ5>3>svm-nGi5v9v*Th zd_q}dfRsrIdrawfafKW9TOJv7F|mP(94DSvbW0f-^dM*I2=Psv=ln?zwrhMIa%}>} zF$+7zlIfQ0%A}eT3=4||j#SH(3>VKb#kWR0mcZgvQYsXQSE%2zJ~z^dmfC@zK?E5R zMuvP6NFxtJ|iXe(?JQxgzg1g3A%Vyx-H5xO?eyq0qB+*1Yxzv~k3bZBD<%SGup%7l)w zhyzKtM#yBmk0(^8qEemQlbD3hb-=P1B^EYCim48QH2l0IOy|(^MBBCLYP_qebcrA) zwOoN`4Ch&-Y*lR6!c4~b0*@mYVIV|In?X8*rqEcRS+-8gQ!*|4Z}yET%ajdrjHh75 zn3BeVr@KrOH0ymSXk`h#$9H_$_6GUkzQK~`XT=DjfM+k`5U;~~V&HXNF~)fESB${# zbZ9CxfW96=Xj++4urmG$)JeJ${r{&-Dg2>Nhkf31V&L$UauarKtXm}vb_M8ipXiot>)_ia-aF=>S+>XRiA}(W zAFmHn*J+sQXm~Mn5OtuXK0aM};U*Gn5-_SsYg~f)+X5qG2 z8mYV6r-LN07taacG(lH^Bg&hbX%6u%9s|=bT)>2wnZ_ZLAIM_$n+T~|a2==UGqrvt zri_z{$HHY=Ecq-#&XNMaGgASE;aVA$e#Fn%#3ymuc79q+>=kgw!gW9JwM4uix=sZ` zXd+5LY`P0HtKsK8hUpxtHK9c7)!xg!=LQ}};(u$2tI5mBN7{yYZNshpyGL&yy)#(V zHaygJJqc9$yr$pE-yOd_e#fb5`a^Bcq87Uvy&Sy~Uu^EEuH98l?XGIOU#X~N_00O~ z>mQ}N=F?q|(%tjv?uB&EC3Ue=e~H4;?U#-`Pj$?mUPx_z(!TEdWbBXHey$_o|UTkieiCvG)?)|>^M(@o7e>Qj5;^3r~K+*WJ(|1%; z*{YTmSa&Sb4tcGVp>CAp8H)^Co)1_}IaW~)bCf5FY2^yGN`A3GIbg9S?~HBPg;C;E zvMzO~#)Zj(OG%d3ly|f!Gv%1gS{t8&tTo|}W`QBLN8_NO39jyVN4@-eM>zs9wGQjz zv%x~pBLcyhQg%IG6l(?8zt(!|9uLs{u%G`0@n&JFg(OM(2d#UCdgf8jQ?%hJ+Vl)< zd4{r2(asktinY#eo7+0q@nGxk-3$FEt9_>)Mo*tpU#XImy7tv81WQdtvUaiaeCMTu z-)wv#OVXC55R%h1PSrH7TYG-(C1XKqugPnq+_h2-!7`Wm@zBqwe?9e!S>5x+eD~l> OF60|KUT`Z>iT?n1$3DFP literal 2230 zcmb7FO>7fK6rNqL*W;(ev;slZ_Rv!uxaHJ0V{Z~twX`Gc%zJO% zy!Xw#pZzHs4Ivm`d?gqER1o@=Gu{Zai5IIt%p!t_WFS+@NfM`JLpFUmpQ+>&!TSuq zspeEcD@MS?Icx@VLBactkQvT}%}6dHc-4rS9k~t($%y($fZ%ZnD1~-Blv+N=rANVs zz80N}(OCKQHkJ(w33=Kid>K7#@8lBypdDG$j#7!L!e7vnjmf=mo^;SHV+x5~R@SU_ z*5b*;AKZ3Qa)X&l#d36qlB5jGhsV!YlMZEWAWN&XLTpe8oTav1vMQjU9Mx^=s^iod zC3()Dvq2a0`HW>yP!8$DELBFWicy7uJR`nc^zh;TtGg%Y(_ zrF5ZvU9re8^T&F@V+nzhh+LA+qNMM%8!!QnK2E2?8SYDC9PtChI=MQ0XmxM8Xqhx! zAk}oiA~by}b3BvI>xBupQQEY~q(SW#wzOU?rK&YI#Hdb21y1%Fn25k_=`5O-*e8y+hsql@+tM;Wv1v@B zvX`qHcDxuqlDsHO=oPku8;-Zh|8ASe#K)ig6+T40z3ivVA8*s}(9g)zau$7@MQFx1 z?IX&>A$V-*TJZmSnfCDo0iiT?K&!o~?ebPn zvc9fsm3!8u9_93TRC&8ct*`vvEbCd{gi z>W1NB-eb6BH&lSybf}Q3a1W?mSDBzS+YQi*v;ej5s)TZAuK&U$Wi`fA-c`p+25)2v zgT};ohPwVt_SEsyt_oK%OHR_ycq|zY8PoWFNLU}Oz_YmimM31B{4F)s%{8z<4!w`< z1~v|#{R>Q&&`Jd1$YUJ29=#e}!UGLFaOdd#lXp+vJ5|R6i+JDKme#=9o&5cYyA$`! zI@T8Pf#rB_ecQfzY=0f^|1Hry+kdlvDKXec3@#;x8;RkC#GWhaa-a4bDZ$-WPCSeC z&YfR~z4^Fj`|aqh=u*#~M$ex4T%+gk4gYd{%WUXoXzt+cBe#xxGxBqMxEX{)%?=a{ zU&mMR*Ii$BEaCnJ?*EqDF5N0UI{e+pgOSF-+Y9)eC7fyC%pyLvj6>IhSA*BW0@qvh zSgMXwqVb(hcDbrktClL`jQ2M;Fs9oMG*AYAS;Y8LxgGWeLpKUVYSvN~GnvYT(SpTj z%1c5KbB6gwXS)}t^hu|f0)`s4h+ZpRy)SKV{9CXvKdy?-&s-nmis;&qid_L%P=*4k|h0(wm(HXpP)Bjeu`30(B2glg*xYU%@57@{xJ08 z@WRn^^~^_$!Sk2YXHwwvzEAtE9Q$m?iY!SxU-*!mXmYBlA{mRl6ARMTrraf^Z}iR` hy8GVzyZ1h*Z`yOB#t5CAq71s4DS delta 19 ZcmZ3Zv_^^RG%qg~0}z => { + try { + const response = await apiClient.post('/admin/cleardb', { hard }); + return response.data; + } catch (error) { + console.error('Error calling clearDatabase API:', error); + // Re-throw the error so the component can handle it + throw error; + } +}; diff --git a/interfaces/nativeapp/src/api/client.ts b/interfaces/nativeapp/src/api/client.ts index 616f07c..4cb43c4 100644 --- a/interfaces/nativeapp/src/api/client.ts +++ b/interfaces/nativeapp/src/api/client.ts @@ -5,8 +5,8 @@ import * as SecureStore from 'expo-secure-store'; import AsyncStorage from '@react-native-async-storage/async-storage'; -const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.255.221:8000/api'; // Use your machine's IP -// const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.1.9:8000/api'; // Use your machine's IP +// const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.255.221:8000/api'; // Use your machine's IP +const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.1.9:8000/api'; // Use your machine's IP // const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8000/api'; // Use your machine's IP const ACCESS_TOKEN_KEY = 'maia_access_token'; // Renamed for clarity const REFRESH_TOKEN_KEY = 'maia_refresh_token'; // Key for refresh token diff --git a/interfaces/nativeapp/src/components/WebSidebar.tsx b/interfaces/nativeapp/src/components/WebSidebar.tsx index 5998464..2fba952 100644 --- a/interfaces/nativeapp/src/components/WebSidebar.tsx +++ b/interfaces/nativeapp/src/components/WebSidebar.tsx @@ -4,26 +4,38 @@ import { View, StyleSheet, Pressable } from 'react-native'; import { Text, useTheme, Icon } from 'react-native-paper'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import { NavigationContainerRef } from '@react-navigation/native'; // Import ref type -import { RootStackParamList } from '../types/navigation'; // Import stack param list +import { WebContentStackParamList } from '../types/navigation'; // Use WebContentStackParamList +import { useAuth, UserRole } from '../contexts/AuthContext'; // Import useAuth and UserRole // Define Props including the navigation ref interface WebSidebarProps { - navigationRef: React.RefObject>; + navigationRef: React.RefObject>; // Use WebContentStackParamList currentRouteName?: string; // To highlight the active item } +// Define navigation items type +type NavItem = { + name: keyof WebContentStackParamList; + icon: string; + label: string; + adminOnly?: boolean; // Add flag for admin-only items +}; + // Define navigation items -const navItems = [ +const navItems: NavItem[] = [ { name: 'Dashboard', icon: 'view-dashboard', label: 'Dashboard' }, { name: 'Chat', icon: 'chat', label: 'Chat' }, { name: 'Calendar', icon: 'calendar', label: 'Calendar' }, { name: 'Profile', icon: 'account-circle', label: 'Profile' }, -] as const; // Use 'as const' for stricter typing of names + { name: 'Admin', icon: 'shield-crown', label: 'Admin', adminOnly: true }, // Add Admin item +]; const WebSidebar = ({ navigationRef, currentRouteName }: WebSidebarProps) => { const theme = useTheme(); + const { user } = useAuth(); // Get user data + const isAdmin = user?.role === UserRole.ADMIN; - const handleNavigate = (screenName: keyof RootStackParamList) => { + const handleNavigate = (screenName: keyof WebContentStackParamList) => { // Use the ref to navigate if (navigationRef.current) { navigationRef.current.navigate(screenName); @@ -73,7 +85,7 @@ const WebSidebar = ({ navigationRef, currentRouteName }: WebSidebarProps) => { color: theme.colors.onPrimary, // Text color on primary background }, inactiveLabel: { - color: theme.colors.textSecondary, // Text color for inactive items + color: theme.colors.onSurfaceVariant, // Text color for inactive items }, }); @@ -85,6 +97,11 @@ const WebSidebar = ({ navigationRef, currentRouteName }: WebSidebarProps) => { {navItems.map((item) => { + // Skip admin item if user is not admin + if (item.adminOnly && !isAdmin) { + return null; + } + const isActive = currentRouteName === item.name; return ( { Promise; logout: () => Promise; } @@ -15,6 +29,7 @@ interface AuthContextData { const AuthContext = createContext({ isAuthenticated: false, isLoading: true, + user: null, // Initialize user as null login: async () => { throw new Error('AuthContext not initialized'); }, logout: async () => { throw new Error('AuthContext not initialized'); }, }); @@ -26,15 +41,45 @@ interface AuthProviderProps { export const AuthProvider: React.FC = ({ children }) => { const [isLoading, setIsLoading] = useState(true); const [isAuthenticatedState, setIsAuthenticatedState] = useState(false); + const [userState, setUserState] = useState(null); // State for user data + + // Function to fetch user data + const fetchUserData = useCallback(async () => { + try { + console.log("[AuthContext] fetchUserData: Fetching /user/me"); + const response = await apiClient.get('/user/me'); + console.log("[AuthContext] fetchUserData: User data received:", response.data); + setUserState(response.data); + return response.data; + } catch (error: any) { + console.error("[AuthContext] fetchUserData: Error fetching user data:", error.response?.data || error.message); + // If fetching user fails (e.g., token expired mid-session), log out + await clearTokens(); + setIsAuthenticatedState(false); + setUserState(null); + return null; + } + }, []); const checkAuthStatus = useCallback(async () => { const token = await getAccessToken(); const hasToken = !!token; - if (hasToken !== isAuthenticatedState) { - setIsAuthenticatedState(hasToken); + if (hasToken) { + console.log("[AuthContext] checkAuthStatus: Token found, fetching user data."); + const userData = await fetchUserData(); + if (userData) { + setIsAuthenticatedState(true); + } else { + // Fetch failed, already handled logout in fetchUserData + setIsAuthenticatedState(false); + } + } else { + console.log("[AuthContext] checkAuthStatus: No token found."); + setIsAuthenticatedState(false); + setUserState(null); } - return hasToken; - }, [isAuthenticatedState]); + return isAuthenticatedState; // Return the updated state + }, [fetchUserData, isAuthenticatedState]); // Added isAuthenticatedState dependency const loadInitialAuth = useCallback(async () => { setIsLoading(true); @@ -43,9 +88,10 @@ export const AuthProvider: React.FC = ({ children }) => { await checkAuthStatus(); console.log("[AuthContext] loadInitialAuth: Initial check complete."); } catch (error) { - console.error("[AuthContext] loadInitialAuth: Error loading initial token:", error); + console.error("[AuthContext] loadInitialAuth: Error during initial auth check:", error); await clearTokens(); setIsAuthenticatedState(false); + setUserState(null); } finally { setIsLoading(false); } @@ -58,7 +104,7 @@ export const AuthProvider: React.FC = ({ children }) => { const login = useCallback(async (username: string, password: string) => { console.log("[AuthContext] login: Function called with:", username); try { - console.log("[AuthContext] login: Preparing to call apiClient.post for /auth/login"); + // ... (existing login API call) ... const response = await apiClient.post( '/auth/login', 'grant_type=password&username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password) + '&scope=&client_id=&client_secret=', @@ -70,10 +116,8 @@ export const AuthProvider: React.FC = ({ children }) => { } ); - console.log("[AuthContext] login: apiClient.post successful, response status:", response?.status); const { access_token, refresh_token } = response.data; - console.log("[AuthContext] login: Response data received."); - + // ... (existing token validation) ... if (!access_token || typeof access_token !== 'string' || !refresh_token) { console.error("[AuthContext] login: Invalid token structure received:", response.data); throw new Error('Invalid token received from server.'); @@ -81,29 +125,30 @@ export const AuthProvider: React.FC = ({ children }) => { console.log('[AuthContext] login: Login successful, storing tokens.'); await storeTokens(access_token, refresh_token); - setIsAuthenticatedState(true); + + // Fetch user data immediately after successful login + const userData = await fetchUserData(); + if (userData) { + setIsAuthenticatedState(true); + } else { + // Should not happen if login succeeded, but handle defensively + throw new Error('Failed to fetch user data after login.'); + } } catch (error: any) { + // ... (existing error handling) ... console.error("[AuthContext] login: Caught Error Object:", error); - if (error.isAxiosError) { - console.error("[AuthContext] login: Axios Error Details:"); - console.error(" Request Config:", error.config); - console.error(" Response:", error.response?.status, error.response?.data); - console.error(" Message:", error.message); - } await clearTokens(); setIsAuthenticatedState(false); + setUserState(null); throw error; } - }, []); + }, [fetchUserData]); // Added fetchUserData dependency const logout = useCallback(async () => { console.log('[AuthContext] logout: Logging out.'); const refreshToken = await getRefreshToken(); - if (!refreshToken) { - console.warn('[AuthContext] logout: No refresh token found to send to backend.'); - } - + // ... (existing backend logout call) ... try { if (refreshToken) { console.log('[AuthContext] logout: Calling backend /auth/logout'); @@ -115,6 +160,7 @@ export const AuthProvider: React.FC = ({ children }) => { } finally { await clearTokens(); setIsAuthenticatedState(false); + setUserState(null); // Clear user data on logout console.log('[AuthContext] logout: Local tokens cleared and state updated.'); } }, []); @@ -122,10 +168,12 @@ export const AuthProvider: React.FC = ({ children }) => { const contextValue = useMemo(() => ({ isAuthenticated: isAuthenticatedState, isLoading, + user: userState, // Provide user state login, logout, - }), [isAuthenticatedState, isLoading, login, logout]); + }), [isAuthenticatedState, isLoading, userState, login, logout]); // Added userState dependency + // ... (rest of the component: Provider, useAuth, AuthLoadingScreen) ... return ( {children} @@ -147,4 +195,7 @@ export const AuthLoadingScreen: React.FC = () => { container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: theme.colors.background } }); return (); -} \ No newline at end of file +} + +// Export UserRole if needed elsewhere +export { UserRole }; \ No newline at end of file diff --git a/interfaces/nativeapp/src/navigation/MobileTabNavigator.tsx b/interfaces/nativeapp/src/navigation/MobileTabNavigator.tsx index d11c709..8954f12 100644 --- a/interfaces/nativeapp/src/navigation/MobileTabNavigator.tsx +++ b/interfaces/nativeapp/src/navigation/MobileTabNavigator.tsx @@ -9,6 +9,8 @@ import ChatScreen from '../screens/ChatScreen'; import CalendarScreen from '../screens/CalendarScreen'; import ProfileScreen from '../screens/ProfileScreen'; import EventFormScreen from '../screens/EventFormScreen'; +import AdminScreen from '../screens/AdminScreen'; // Import AdminScreen +import { useAuth, UserRole } from '../contexts/AuthContext'; // Import useAuth and UserRole import { MobileTabParamList } from '../types/navigation'; @@ -16,13 +18,15 @@ const Tab = createBottomTabNavigator(); const MobileTabNavigator = () => { const theme = useTheme(); + const { user } = useAuth(); // Get user data from context + const isAdmin = user?.role === UserRole.ADMIN; return ( ({ tabBarActiveTintColor: theme.colors.secondary, - tabBarInactiveTintColor: theme.colors.textSecondary, + tabBarInactiveTintColor: theme.colors.onSurfaceVariant, // Use onSurfaceVariant instead of textSecondary tabBarStyle: { backgroundColor: theme.colors.surface, borderTopColor: theme.colors.surface, // Or a subtle border @@ -30,7 +34,7 @@ const MobileTabNavigator = () => { headerStyle: { backgroundColor: theme.colors.surface, }, - headerTintColor: theme.colors.text, + headerTintColor: theme.colors.onSurface, // Use onSurface instead of text tabBarIcon: ({ focused, color, size }) => { let iconName: string = 'help-circle'; // Default icon @@ -42,6 +46,8 @@ const MobileTabNavigator = () => { iconName = focused ? 'calendar' : 'calendar-outline'; } else if (route.name === 'ProfileTab') { iconName = focused ? 'account-circle' : 'account-circle-outline'; + } else if (route.name === 'AdminTab') { // Add icon for AdminTab + iconName = focused ? 'shield-crown' : 'shield-crown-outline'; } return ; }, @@ -67,6 +73,14 @@ const MobileTabNavigator = () => { component={ProfileScreen} options={{ title: 'Profile', headerTitle: 'Profile' }} /> + {/* Conditionally render Admin Tab */} + {isAdmin && ( + + )} ); }; diff --git a/interfaces/nativeapp/src/navigation/WebContentNavigator.tsx b/interfaces/nativeapp/src/navigation/WebContentNavigator.tsx index b61a361..24d049d 100644 --- a/interfaces/nativeapp/src/navigation/WebContentNavigator.tsx +++ b/interfaces/nativeapp/src/navigation/WebContentNavigator.tsx @@ -8,6 +8,8 @@ import ChatScreen from '../screens/ChatScreen'; import CalendarScreen from '../screens/CalendarScreen'; import ProfileScreen from '../screens/ProfileScreen'; import EventFormScreen from '../screens/EventFormScreen'; +import AdminScreen from '../screens/AdminScreen'; // Import AdminScreen +import { useAuth, UserRole } from '../contexts/AuthContext'; // Import useAuth and UserRole import { WebContentStackParamList } from '../types/navigation'; @@ -15,6 +17,8 @@ const Stack = createNativeStackNavigator(); const WebContentNavigator = () => { const theme = useTheme(); + const { user } = useAuth(); // Get user data + const isAdmin = user?.role === UserRole.ADMIN; return ( { headerStyle: { backgroundColor: theme.colors.surface, }, - headerTintColor: theme.colors.text, + headerTintColor: theme.colors.onSurface, // Use onSurface instead of text headerTitleStyle: { fontWeight: 'bold', }, @@ -39,6 +43,10 @@ const WebContentNavigator = () => { + {/* Conditionally add Admin screen */} + {isAdmin && ( + + )} ); }; diff --git a/interfaces/nativeapp/src/screens/AdminScreen.tsx b/interfaces/nativeapp/src/screens/AdminScreen.tsx new file mode 100644 index 0000000..9785771 --- /dev/null +++ b/interfaces/nativeapp/src/screens/AdminScreen.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Button, Checkbox, Text, ActivityIndicator, Snackbar } from 'react-native-paper'; +import { clearDatabase } from '../api/admin'; // Revert to standard import without extension + +const AdminScreen = () => { + const [isHardClear, setIsHardClear] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [snackbarVisible, setSnackbarVisible] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(''); + + const handleClearDb = async () => { + setIsLoading(true); + setSnackbarVisible(false); + try { + const response = await clearDatabase(isHardClear); + setSnackbarMessage(response.message || 'Database cleared successfully.'); + setSnackbarVisible(true); + } catch (error: any) { + console.error("Error clearing database:", error); + setSnackbarMessage(error.response?.data?.detail || 'Failed to clear database.'); + setSnackbarVisible(true); + } finally { + setIsLoading(false); + } + }; + + return ( + + Admin Controls + + + setIsHardClear(!isHardClear)} + /> + setIsHardClear(!isHardClear)}>Hard Clear (Delete all data) + + + + + setSnackbarVisible(false)} + duration={Snackbar.DURATION_SHORT} + > + {snackbarMessage} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 20, + justifyContent: 'center', + alignItems: 'center', + }, + title: { + marginBottom: 30, + }, + checkboxContainer: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 20, + }, + button: { + marginTop: 10, + }, +}); + +export default AdminScreen; diff --git a/interfaces/nativeapp/src/types/navigation.ts b/interfaces/nativeapp/src/types/navigation.ts index 7213f67..27fd3eb 100644 --- a/interfaces/nativeapp/src/types/navigation.ts +++ b/interfaces/nativeapp/src/types/navigation.ts @@ -7,6 +7,7 @@ export type MobileTabParamList = { ChatTab: undefined; CalendarTab: undefined; ProfileTab: undefined; + AdminTab?: undefined; // Add Admin tab (optional) }; // Screens within the Web Content Area Stack Navigator @@ -15,6 +16,7 @@ export type WebContentStackParamList = { Chat: undefined; Calendar: undefined; Profile: undefined; + Admin?: undefined; // Add Admin screen EventForm?: { eventId?: number; selectedDate?: string }; };