diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 4d39bec..71dd5c0 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -26,12 +26,19 @@ export default function TabLayout() { ( - + ), }} /> + {/* Hide dashboard - now accessed via beneficiary selection */} + ([ { id: '1', @@ -44,7 +46,13 @@ export default function ChatScreen() { setIsSending(true); try { - const response = await api.sendMessage(trimmedInput); + // Prepend beneficiary context to the question if available + const beneficiaryContext = getBeneficiaryContext(); + const questionWithContext = beneficiaryContext + ? `${beneficiaryContext} ${trimmedInput}` + : trimmedInput; + + const response = await api.sendMessage(questionWithContext); if (response.ok && response.data?.response) { const assistantMessage: Message = { @@ -74,7 +82,7 @@ export default function ChatScreen() { } finally { setIsSending(false); } - }, [input, isSending]); + }, [input, isSending, getBeneficiaryContext]); const renderMessage = ({ item }: { item: Message }) => { const isUser = item.role === 'user'; @@ -124,7 +132,11 @@ export default function ChatScreen() { Julia AI - {isSending ? 'Typing...' : 'Online'} + {isSending + ? 'Typing...' + : currentBeneficiary + ? `About ${currentBeneficiary.name}` + : 'Online'} diff --git a/app/(tabs)/dashboard.tsx b/app/(tabs)/dashboard.tsx new file mode 100644 index 0000000..e2c51c1 --- /dev/null +++ b/app/(tabs)/dashboard.tsx @@ -0,0 +1,162 @@ +import React, { useState, useRef } from 'react'; +import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native'; +import { WebView } from 'react-native-webview'; +import { Ionicons } from '@expo/vector-icons'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import * as SecureStore from 'expo-secure-store'; +import { AppColors, FontSizes, Spacing } from '@/constants/theme'; +import { FullScreenError } from '@/components/ui/ErrorMessage'; + +const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard'; + +export default function DashboardScreen() { + const webViewRef = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [canGoBack, setCanGoBack] = useState(false); + + const handleRefresh = () => { + setError(null); + setIsLoading(true); + webViewRef.current?.reload(); + }; + + const handleBack = () => { + if (canGoBack) { + webViewRef.current?.goBack(); + } + }; + + const handleNavigationStateChange = (navState: any) => { + setCanGoBack(navState.canGoBack); + }; + + const handleError = () => { + setError('Failed to load dashboard. Please check your internet connection.'); + setIsLoading(false); + }; + + if (error) { + return ( + + + Dashboard + + + + ); + } + + return ( + + {/* Header */} + + {canGoBack && ( + + + + )} + + Dashboard + + + + + + + {/* WebView */} + + setIsLoading(true)} + onLoadEnd={() => setIsLoading(false)} + onError={handleError} + onHttpError={handleError} + onNavigationStateChange={handleNavigationStateChange} + javaScriptEnabled={true} + domStorageEnabled={true} + startInLoadingState={true} + scalesPageToFit={true} + allowsBackForwardNavigationGestures={true} + renderLoading={() => ( + + + Loading dashboard... + + )} + /> + + {isLoading && ( + + + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: AppColors.background, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: Spacing.md, + paddingVertical: Spacing.sm, + backgroundColor: AppColors.background, + borderBottomWidth: 1, + borderBottomColor: AppColors.border, + }, + backButton: { + padding: Spacing.xs, + marginRight: Spacing.sm, + }, + headerTitle: { + flex: 1, + fontSize: FontSizes.xl, + fontWeight: '700', + color: AppColors.textPrimary, + }, + headerTitleWithBack: { + marginLeft: 0, + }, + refreshButton: { + padding: Spacing.xs, + }, + webViewContainer: { + flex: 1, + }, + webView: { + flex: 1, + }, + loadingContainer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: AppColors.background, + }, + loadingOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(255,255,255,0.8)', + }, + loadingText: { + marginTop: Spacing.md, + fontSize: FontSizes.base, + color: AppColors.textSecondary, + }, +}); diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 5f1a03d..1eb609e 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -12,29 +12,31 @@ import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import { api } from '@/services/api'; import { useAuth } from '@/contexts/AuthContext'; +import { useBeneficiary } from '@/contexts/BeneficiaryContext'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { FullScreenError } from '@/components/ui/ErrorMessage'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; -import type { Patient } from '@/types'; +import type { Beneficiary } from '@/types'; -export default function PatientsListScreen() { +export default function BeneficiariesListScreen() { const { user } = useAuth(); - const [patients, setPatients] = useState([]); + const { setCurrentBeneficiary } = useBeneficiary(); + const [beneficiaries, setBeneficiaries] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); - const loadPatients = useCallback(async (showLoading = true) => { + const loadBeneficiaries = useCallback(async (showLoading = true) => { if (showLoading) setIsLoading(true); setError(null); try { - const response = await api.getPatients(); + const response = await api.getBeneficiaries(); if (response.ok && response.data) { - setPatients(response.data.patients); + setBeneficiaries(response.data.beneficiaries); } else { - setError(response.error?.message || 'Failed to load patients'); + setError(response.error?.message || 'Failed to load beneficiaries'); } } catch (err) { setError(err instanceof Error ? err.message : 'An error occurred'); @@ -45,25 +47,28 @@ export default function PatientsListScreen() { }, []); useEffect(() => { - loadPatients(); - }, [loadPatients]); + loadBeneficiaries(); + }, [loadBeneficiaries]); const handleRefresh = useCallback(() => { setIsRefreshing(true); - loadPatients(false); - }, [loadPatients]); + loadBeneficiaries(false); + }, [loadBeneficiaries]); - const handlePatientPress = (patient: Patient) => { - router.push(`/patients/${patient.id}`); + const handleBeneficiaryPress = (beneficiary: Beneficiary) => { + // Set current beneficiary in context before navigating + setCurrentBeneficiary(beneficiary); + // Navigate directly to their dashboard + router.push(`/beneficiaries/${beneficiary.id}/dashboard`); }; - const renderPatientCard = ({ item }: { item: Patient }) => ( + const renderBeneficiaryCard = ({ item }: { item: Beneficiary }) => ( handlePatientPress(item)} + style={styles.beneficiaryCard} + onPress={() => handleBeneficiaryPress(item)} activeOpacity={0.7} > - + {item.name.charAt(0).toUpperCase()} @@ -76,9 +81,9 @@ export default function PatientsListScreen() { /> - - {item.name} - {item.relationship} + + {item.name} + {item.relationship} {' '} {item.last_activity} @@ -86,22 +91,34 @@ export default function PatientsListScreen() { - {item.health_data && ( - + {item.sensor_data && ( + - - {item.health_data.heart_rate} - BPM + + + {item.sensor_data.motion_detected ? 'Active' : 'Inactive'} + + Motion - - {item.health_data.steps?.toLocaleString()} - Steps + + + {item.sensor_data.door_status === 'open' ? 'Open' : 'Closed'} + + Door - - {item.health_data.sleep_hours}h - Sleep + + {item.sensor_data.temperature}° + Temp )} @@ -116,11 +133,11 @@ export default function PatientsListScreen() { ); if (isLoading) { - return ; + return ; } if (error) { - return loadPatients()} />; + return loadBeneficiaries()} />; } return ( @@ -131,18 +148,18 @@ export default function PatientsListScreen() { Hello, {user?.user_name || 'User'} - Your Patients + Beneficiaries - {/* Patient List */} + {/* Beneficiary List */} item.id.toString()} - renderItem={renderPatientCard} + renderItem={renderBeneficiaryCard} contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={false} refreshControl={ @@ -155,9 +172,9 @@ export default function PatientsListScreen() { ListEmptyComponent={ - No patients yet + No beneficiaries yet - Add your first patient to start monitoring + Add your first beneficiary to start monitoring } @@ -201,7 +218,7 @@ const styles = StyleSheet.create({ listContent: { padding: Spacing.md, }, - patientCard: { + beneficiaryCard: { backgroundColor: AppColors.background, borderRadius: BorderRadius.lg, padding: Spacing.md, @@ -212,7 +229,7 @@ const styles = StyleSheet.create({ shadowRadius: 4, elevation: 2, }, - patientInfo: { + beneficiaryInfo: { flexDirection: 'row', alignItems: 'center', marginBottom: Spacing.md, @@ -247,15 +264,15 @@ const styles = StyleSheet.create({ offline: { backgroundColor: AppColors.offline, }, - patientDetails: { + beneficiaryDetails: { flex: 1, }, - patientName: { + beneficiaryName: { fontSize: FontSizes.lg, fontWeight: '600', color: AppColors.textPrimary, }, - patientRelationship: { + beneficiaryRelationship: { fontSize: FontSizes.sm, color: AppColors.textSecondary, marginTop: 2, @@ -265,7 +282,7 @@ const styles = StyleSheet.create({ color: AppColors.textMuted, marginTop: 4, }, - healthStats: { + sensorStats: { flexDirection: 'row', justifyContent: 'space-around', paddingTop: Spacing.md, diff --git a/app/_layout.tsx b/app/_layout.tsx index 6824850..6eab4c0 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -7,6 +7,7 @@ import 'react-native-reanimated'; import { useColorScheme } from '@/hooks/use-color-scheme'; import { AuthProvider, useAuth } from '@/contexts/AuthContext'; +import { BeneficiaryProvider } from '@/contexts/BeneficiaryContext'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; // Prevent auto-hiding splash screen @@ -43,7 +44,7 @@ function RootLayoutNav() { - + @@ -54,7 +55,9 @@ function RootLayoutNav() { export default function RootLayout() { return ( - + + + ); } diff --git a/app/beneficiaries/[id]/dashboard.tsx b/app/beneficiaries/[id]/dashboard.tsx new file mode 100644 index 0000000..03ccf53 --- /dev/null +++ b/app/beneficiaries/[id]/dashboard.tsx @@ -0,0 +1,263 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native'; +import { WebView } from 'react-native-webview'; +import { Ionicons } from '@expo/vector-icons'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useLocalSearchParams, router } from 'expo-router'; +import { useBeneficiary } from '@/contexts/BeneficiaryContext'; +import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; +import { FullScreenError } from '@/components/ui/ErrorMessage'; + +const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard'; + +export default function BeneficiaryDashboardScreen() { + const { id } = useLocalSearchParams<{ id: string }>(); + const { currentBeneficiary, setCurrentBeneficiary } = useBeneficiary(); + const webViewRef = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [canGoBack, setCanGoBack] = useState(false); + + const beneficiaryName = currentBeneficiary?.name || 'Dashboard'; + + const handleRefresh = () => { + setError(null); + setIsLoading(true); + webViewRef.current?.reload(); + }; + + const handleWebViewBack = () => { + if (canGoBack) { + webViewRef.current?.goBack(); + } + }; + + const handleNavigationStateChange = (navState: any) => { + setCanGoBack(navState.canGoBack); + }; + + const handleError = () => { + setError('Failed to load dashboard. Please check your internet connection.'); + setIsLoading(false); + }; + + const handleGoBack = () => { + router.back(); + }; + + if (error) { + return ( + + + + + + {beneficiaryName} + + + + + ); + } + + return ( + + {/* Header */} + + + + + + + {currentBeneficiary && ( + + + {currentBeneficiary.name.charAt(0).toUpperCase()} + + + )} + + {beneficiaryName} + {currentBeneficiary?.relationship && ( + {currentBeneficiary.relationship} + )} + + + + + {canGoBack && ( + + + + )} + + + + + + + {/* WebView */} + + setIsLoading(true)} + onLoadEnd={() => setIsLoading(false)} + onError={handleError} + onHttpError={handleError} + onNavigationStateChange={handleNavigationStateChange} + javaScriptEnabled={true} + domStorageEnabled={true} + startInLoadingState={true} + scalesPageToFit={true} + allowsBackForwardNavigationGestures={true} + renderLoading={() => ( + + + Loading dashboard... + + )} + /> + + {isLoading && ( + + + + )} + + + {/* Bottom Quick Actions */} + + { + if (currentBeneficiary) { + setCurrentBeneficiary(currentBeneficiary); + } + router.push('/(tabs)/chat'); + }} + > + + Ask Julia + + + router.push(`/beneficiaries/${id}`)} + > + + Details + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: AppColors.background, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: Spacing.md, + paddingVertical: Spacing.sm, + backgroundColor: AppColors.background, + borderBottomWidth: 1, + borderBottomColor: AppColors.border, + }, + backButton: { + padding: Spacing.xs, + }, + headerCenter: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + marginLeft: Spacing.sm, + }, + avatarSmall: { + width: 36, + height: 36, + borderRadius: BorderRadius.full, + backgroundColor: AppColors.primaryLight, + justifyContent: 'center', + alignItems: 'center', + marginRight: Spacing.sm, + }, + avatarText: { + fontSize: FontSizes.base, + fontWeight: '600', + color: AppColors.white, + }, + headerTitle: { + fontSize: FontSizes.lg, + fontWeight: '700', + color: AppColors.textPrimary, + }, + headerSubtitle: { + fontSize: FontSizes.xs, + color: AppColors.textSecondary, + }, + headerActions: { + flexDirection: 'row', + alignItems: 'center', + }, + actionButton: { + padding: Spacing.xs, + marginLeft: Spacing.xs, + }, + placeholder: { + width: 32, + }, + webViewContainer: { + flex: 1, + }, + webView: { + flex: 1, + }, + loadingContainer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: AppColors.background, + }, + loadingOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(255,255,255,0.8)', + }, + loadingText: { + marginTop: Spacing.md, + fontSize: FontSizes.base, + color: AppColors.textSecondary, + }, + bottomBar: { + flexDirection: 'row', + justifyContent: 'space-around', + paddingVertical: Spacing.sm, + paddingHorizontal: Spacing.lg, + backgroundColor: AppColors.background, + borderTopWidth: 1, + borderTopColor: AppColors.border, + }, + quickAction: { + alignItems: 'center', + padding: Spacing.sm, + }, + quickActionText: { + fontSize: FontSizes.xs, + color: AppColors.primary, + marginTop: Spacing.xs, + }, +}); diff --git a/app/patients/[id]/index.tsx b/app/beneficiaries/[id]/index.tsx similarity index 72% rename from app/patients/[id]/index.tsx rename to app/beneficiaries/[id]/index.tsx index b1eef70..6242fec 100644 --- a/app/patients/[id]/index.tsx +++ b/app/beneficiaries/[id]/index.tsx @@ -11,32 +11,34 @@ import { useLocalSearchParams, router } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import { api } from '@/services/api'; +import { useBeneficiary } from '@/contexts/BeneficiaryContext'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { FullScreenError } from '@/components/ui/ErrorMessage'; import { Button } from '@/components/ui/Button'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; -import type { Patient } from '@/types'; +import type { Beneficiary } from '@/types'; -export default function PatientDashboardScreen() { +export default function BeneficiaryDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>(); - const [patient, setPatient] = useState(null); + const { setCurrentBeneficiary } = useBeneficiary(); + const [beneficiary, setBeneficiary] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); - const loadPatient = useCallback(async (showLoading = true) => { + const loadBeneficiary = useCallback(async (showLoading = true) => { if (!id) return; if (showLoading) setIsLoading(true); setError(null); try { - const response = await api.getPatient(parseInt(id, 10)); + const response = await api.getBeneficiary(parseInt(id, 10)); if (response.ok && response.data) { - setPatient(response.data); + setBeneficiary(response.data); } else { - setError(response.error?.message || 'Failed to load patient'); + setError(response.error?.message || 'Failed to load beneficiary'); } } catch (err) { setError(err instanceof Error ? err.message : 'An error occurred'); @@ -47,27 +49,32 @@ export default function PatientDashboardScreen() { }, [id]); useEffect(() => { - loadPatient(); - }, [loadPatient]); + loadBeneficiary(); + }, [loadBeneficiary]); const handleRefresh = useCallback(() => { setIsRefreshing(true); - loadPatient(false); - }, [loadPatient]); + loadBeneficiary(false); + }, [loadBeneficiary]); const handleChatPress = () => { + // Set current beneficiary in context before navigating to chat + // This allows the chat to include beneficiary context in AI questions + if (beneficiary) { + setCurrentBeneficiary(beneficiary); + } router.push('/(tabs)/chat'); }; if (isLoading) { - return ; + return ; } - if (error || !patient) { + if (error || !beneficiary) { return ( loadPatient()} + message={error || 'Beneficiary not found'} + onRetry={() => loadBeneficiary()} /> ); } @@ -82,7 +89,7 @@ export default function PatientDashboardScreen() { > - {patient.name} + {beneficiary.name} @@ -99,67 +106,75 @@ export default function PatientDashboardScreen() { /> } > - {/* Patient Info Card */} + {/* Beneficiary Info Card */} - {patient.name.charAt(0).toUpperCase()} + {beneficiary.name.charAt(0).toUpperCase()} - {patient.status === 'online' ? 'Online' : 'Offline'} + {beneficiary.status === 'online' ? 'Online' : 'Offline'} - {patient.name} - {patient.relationship} + {beneficiary.name} + {beneficiary.relationship} - Last activity: {patient.last_activity} + Last activity: {beneficiary.last_activity} - {/* Health Stats */} + {/* Sensor Stats */} - Health Overview + Sensor Overview - - + + - {patient.health_data?.heart_rate || '--'} + {beneficiary.sensor_data?.motion_detected ? 'Active' : 'Inactive'} - Heart Rate - BPM + Motion + {beneficiary.sensor_data?.last_motion || '--'} - - + + - {patient.health_data?.steps?.toLocaleString() || '--'} + {beneficiary.sensor_data?.door_status === 'open' ? 'Open' : 'Closed'} - Steps Today - steps + Door Status + Main entrance - + - {patient.health_data?.sleep_hours || '--'} + {beneficiary.sensor_data?.temperature || '--'}°C - Sleep - hours + Temperature + {beneficiary.sensor_data?.humidity || '--'}% humidity @@ -192,9 +207,9 @@ export default function PatientDashboardScreen() { - + - Medications + Activity Report @@ -280,7 +295,7 @@ const styles = StyleSheet.create({ fontWeight: '500', color: AppColors.white, }, - patientName: { + beneficiaryName: { fontSize: FontSizes['2xl'], fontWeight: '700', color: AppColors.textPrimary, diff --git a/app/patients/_layout.tsx b/app/beneficiaries/_layout.tsx similarity index 76% rename from app/patients/_layout.tsx rename to app/beneficiaries/_layout.tsx index df580ec..b323b95 100644 --- a/app/patients/_layout.tsx +++ b/app/beneficiaries/_layout.tsx @@ -1,7 +1,7 @@ import { Stack } from 'expo-router'; import { AppColors } from '@/constants/theme'; -export default function PatientsLayout() { +export default function BeneficiariesLayout() { return ( + ); } diff --git a/contexts/BeneficiaryContext.tsx b/contexts/BeneficiaryContext.tsx new file mode 100644 index 0000000..f769228 --- /dev/null +++ b/contexts/BeneficiaryContext.tsx @@ -0,0 +1,86 @@ +import React, { createContext, useContext, useState, useCallback } from 'react'; +import type { Beneficiary } from '@/types'; + +interface BeneficiaryContextType { + currentBeneficiary: Beneficiary | null; + setCurrentBeneficiary: (beneficiary: Beneficiary | null) => void; + clearCurrentBeneficiary: () => void; + // Helper to format beneficiary context for AI + getBeneficiaryContext: () => string; +} + +const BeneficiaryContext = createContext(undefined); + +export function BeneficiaryProvider({ children }: { children: React.ReactNode }) { + const [currentBeneficiary, setCurrentBeneficiary] = useState(null); + + const clearCurrentBeneficiary = useCallback(() => { + setCurrentBeneficiary(null); + }, []); + + const getBeneficiaryContext = useCallback(() => { + if (!currentBeneficiary) { + return ''; + } + + const parts = [`[Context: Asking about ${currentBeneficiary.name}`]; + + if (currentBeneficiary.relationship) { + parts.push(`(${currentBeneficiary.relationship})`); + } + + if (currentBeneficiary.sensor_data) { + const sensor = currentBeneficiary.sensor_data; + const sensorInfo: string[] = []; + + if (sensor.motion_detected !== undefined) { + sensorInfo.push(`motion: ${sensor.motion_detected ? 'active' : 'inactive'}`); + } + if (sensor.last_motion) { + sensorInfo.push(`last motion: ${sensor.last_motion}`); + } + if (sensor.door_status) { + sensorInfo.push(`door: ${sensor.door_status}`); + } + if (sensor.temperature !== undefined) { + sensorInfo.push(`temp: ${sensor.temperature}°C`); + } + if (sensor.humidity !== undefined) { + sensorInfo.push(`humidity: ${sensor.humidity}%`); + } + + if (sensorInfo.length > 0) { + parts.push(`| Sensors: ${sensorInfo.join(', ')}`); + } + } + + if (currentBeneficiary.last_activity) { + parts.push(`| Last activity: ${currentBeneficiary.last_activity}`); + } + + parts.push(']'); + + return parts.join(' '); + }, [currentBeneficiary]); + + return ( + + {children} + + ); +} + +export function useBeneficiary() { + const context = useContext(BeneficiaryContext); + if (context === undefined) { + throw new Error('useBeneficiary must be used within a BeneficiaryProvider'); + } + return context; +} diff --git a/package-lock.json b/package-lock.json index 20857bb..f6196ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-web": "~0.21.0", + "react-native-webview": "^13.16.0", "react-native-worklets": "0.5.1" }, "devDependencies": { @@ -10393,6 +10394,20 @@ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, + "node_modules/react-native-webview": { + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.0.tgz", + "integrity": "sha512-Nh13xKZWW35C0dbOskD7OX01nQQavOzHbCw9XoZmar4eXCo7AvrYJ0jlUfRVVIJzqINxHlpECYLdmAdFsl9xDA==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "invariant": "2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-worklets": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz", diff --git a/package.json b/package.json index cd6ea60..bcc3db2 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-web": "~0.21.0", + "react-native-webview": "^13.16.0", "react-native-worklets": "0.5.1" }, "devDependencies": { diff --git a/services/api.ts b/services/api.ts index 79bb774..ac61606 100644 --- a/services/api.ts +++ b/services/api.ts @@ -1,5 +1,5 @@ import * as SecureStore from 'expo-secure-store'; -import type { AuthResponse, ChatResponse, Patient, ApiResponse, ApiError } from '@/types'; +import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError } from '@/types'; const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api'; const CLIENT_ID = 'MA_001'; @@ -118,26 +118,28 @@ class ApiService { } } - // Patients - async getPatients(): Promise> { + // Beneficiaries (elderly people being monitored) + async getBeneficiaries(): Promise> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } - // Note: Using mock data since get_patients API structure is not fully documented + // Note: Using mock data since API structure is not fully documented // Replace with actual API call when available - const mockPatients: Patient[] = [ + const mockBeneficiaries: Beneficiary[] = [ { id: 1, name: 'Julia Smith', status: 'online', relationship: 'Mother', last_activity: '2 min ago', - health_data: { - heart_rate: 72, - steps: 3450, - sleep_hours: 7.5, + sensor_data: { + motion_detected: true, + last_motion: '2 min ago', + door_status: 'closed', + temperature: 22, + humidity: 45, }, }, { @@ -146,29 +148,31 @@ class ApiService { status: 'offline', relationship: 'Father', last_activity: '1 hour ago', - health_data: { - heart_rate: 68, - steps: 2100, - sleep_hours: 6.8, + sensor_data: { + motion_detected: false, + last_motion: '1 hour ago', + door_status: 'closed', + temperature: 21, + humidity: 50, }, }, ]; - return { data: { patients: mockPatients }, ok: true }; + return { data: { beneficiaries: mockBeneficiaries }, ok: true }; } - async getPatient(id: number): Promise> { - const response = await this.getPatients(); + async getBeneficiary(id: number): Promise> { + const response = await this.getBeneficiaries(); if (!response.ok || !response.data) { return { ok: false, error: response.error }; } - const patient = response.data.patients.find((p) => p.id === id); - if (!patient) { - return { ok: false, error: { message: 'Patient not found', code: 'NOT_FOUND' } }; + const beneficiary = response.data.beneficiaries.find((b) => b.id === id); + if (!beneficiary) { + return { ok: false, error: { message: 'Beneficiary not found', code: 'NOT_FOUND' } }; } - return { data: patient, ok: true }; + return { data: beneficiary, ok: true }; } // AI Chat diff --git a/types/index.ts b/types/index.ts index 6d9ed9a..193d8b9 100644 --- a/types/index.ts +++ b/types/index.ts @@ -19,8 +19,8 @@ export interface LoginCredentials { password: string; } -// Patient Types -export interface Patient { +// Beneficiary Types (elderly people being monitored) +export interface Beneficiary { id: number; name: string; avatar?: string; @@ -28,13 +28,15 @@ export interface Patient { status: 'online' | 'offline'; relationship?: string; last_activity?: string; - health_data?: HealthData; + sensor_data?: SensorData; } -export interface HealthData { - heart_rate?: number; - steps?: number; - sleep_hours?: number; +export interface SensorData { + motion_detected?: boolean; + last_motion?: string; + door_status?: 'open' | 'closed'; + temperature?: number; + humidity?: number; last_updated?: string; }