Add scheme files, beneficiaries module, dashboard improvements
Changes: - Add wellnuoSheme/ folder with project documentation - Rename patients -> beneficiaries (proper WellNuo terminology) - Add BeneficiaryContext for state management - Update API service with WellNuo endpoints - Add dashboard screen for beneficiary overview - Update navigation and layout Scheme files include: - API documentation with credentials - Project description - System analysis - UX flow - Legal documents (privacy, terms, support) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a804a82512
commit
915664d4cc
@ -26,12 +26,19 @@ export default function TabLayout() {
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Patients',
|
||||
title: 'Home',
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="people" size={size} color={color} />
|
||||
<Ionicons name="home" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{/* Hide dashboard - now accessed via beneficiary selection */}
|
||||
<Tabs.Screen
|
||||
name="dashboard"
|
||||
options={{
|
||||
href: null,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="chat"
|
||||
options={{
|
||||
|
||||
@ -12,10 +12,12 @@ import {
|
||||
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 { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
||||
import type { Message } from '@/types';
|
||||
|
||||
export default function ChatScreen() {
|
||||
const { currentBeneficiary, getBeneficiaryContext } = useBeneficiary();
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
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() {
|
||||
<View>
|
||||
<Text style={styles.headerTitle}>Julia AI</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
{isSending ? 'Typing...' : 'Online'}
|
||||
{isSending
|
||||
? 'Typing...'
|
||||
: currentBeneficiary
|
||||
? `About ${currentBeneficiary.name}`
|
||||
: 'Online'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
162
app/(tabs)/dashboard.tsx
Normal file
162
app/(tabs)/dashboard.tsx
Normal file
@ -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<WebView>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerTitle}>Dashboard</Text>
|
||||
</View>
|
||||
<FullScreenError message={error} onRetry={handleRefresh} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
{canGoBack && (
|
||||
<TouchableOpacity style={styles.backButton} onPress={handleBack}>
|
||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Text style={[styles.headerTitle, canGoBack && styles.headerTitleWithBack]}>
|
||||
Dashboard
|
||||
</Text>
|
||||
<TouchableOpacity style={styles.refreshButton} onPress={handleRefresh}>
|
||||
<Ionicons name="refresh" size={22} color={AppColors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* WebView */}
|
||||
<View style={styles.webViewContainer}>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={{ uri: DASHBOARD_URL }}
|
||||
style={styles.webView}
|
||||
onLoadStart={() => setIsLoading(true)}
|
||||
onLoadEnd={() => setIsLoading(false)}
|
||||
onError={handleError}
|
||||
onHttpError={handleError}
|
||||
onNavigationStateChange={handleNavigationStateChange}
|
||||
javaScriptEnabled={true}
|
||||
domStorageEnabled={true}
|
||||
startInLoadingState={true}
|
||||
scalesPageToFit={true}
|
||||
allowsBackForwardNavigationGestures={true}
|
||||
renderLoading={() => (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||
<Text style={styles.loadingText}>Loading dashboard...</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<View style={styles.loadingOverlay}>
|
||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
@ -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<Patient[]>([]);
|
||||
const { setCurrentBeneficiary } = useBeneficiary();
|
||||
const [beneficiaries, setBeneficiaries] = useState<Beneficiary[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 }) => (
|
||||
<TouchableOpacity
|
||||
style={styles.patientCard}
|
||||
onPress={() => handlePatientPress(item)}
|
||||
style={styles.beneficiaryCard}
|
||||
onPress={() => handleBeneficiaryPress(item)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.patientInfo}>
|
||||
<View style={styles.beneficiaryInfo}>
|
||||
<View style={styles.avatarContainer}>
|
||||
<Text style={styles.avatarText}>
|
||||
{item.name.charAt(0).toUpperCase()}
|
||||
@ -76,9 +81,9 @@ export default function PatientsListScreen() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.patientDetails}>
|
||||
<Text style={styles.patientName}>{item.name}</Text>
|
||||
<Text style={styles.patientRelationship}>{item.relationship}</Text>
|
||||
<View style={styles.beneficiaryDetails}>
|
||||
<Text style={styles.beneficiaryName}>{item.name}</Text>
|
||||
<Text style={styles.beneficiaryRelationship}>{item.relationship}</Text>
|
||||
<Text style={styles.lastActivity}>
|
||||
<Ionicons name="time-outline" size={12} color={AppColors.textMuted} />{' '}
|
||||
{item.last_activity}
|
||||
@ -86,22 +91,34 @@ export default function PatientsListScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{item.health_data && (
|
||||
<View style={styles.healthStats}>
|
||||
{item.sensor_data && (
|
||||
<View style={styles.sensorStats}>
|
||||
<View style={styles.statItem}>
|
||||
<Ionicons name="heart-outline" size={16} color={AppColors.error} />
|
||||
<Text style={styles.statValue}>{item.health_data.heart_rate}</Text>
|
||||
<Text style={styles.statLabel}>BPM</Text>
|
||||
<Ionicons
|
||||
name={item.sensor_data.motion_detected ? "walk" : "walk-outline"}
|
||||
size={16}
|
||||
color={item.sensor_data.motion_detected ? AppColors.online : AppColors.textMuted}
|
||||
/>
|
||||
<Text style={styles.statValue}>
|
||||
{item.sensor_data.motion_detected ? 'Active' : 'Inactive'}
|
||||
</Text>
|
||||
<Text style={styles.statLabel}>Motion</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Ionicons name="footsteps-outline" size={16} color={AppColors.primary} />
|
||||
<Text style={styles.statValue}>{item.health_data.steps?.toLocaleString()}</Text>
|
||||
<Text style={styles.statLabel}>Steps</Text>
|
||||
<Ionicons
|
||||
name={item.sensor_data.door_status === 'open' ? "enter-outline" : "home-outline"}
|
||||
size={16}
|
||||
color={item.sensor_data.door_status === 'open' ? AppColors.warning : AppColors.primary}
|
||||
/>
|
||||
<Text style={styles.statValue}>
|
||||
{item.sensor_data.door_status === 'open' ? 'Open' : 'Closed'}
|
||||
</Text>
|
||||
<Text style={styles.statLabel}>Door</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Ionicons name="moon-outline" size={16} color={AppColors.primaryDark} />
|
||||
<Text style={styles.statValue}>{item.health_data.sleep_hours}h</Text>
|
||||
<Text style={styles.statLabel}>Sleep</Text>
|
||||
<Ionicons name="thermometer-outline" size={16} color={AppColors.primaryDark} />
|
||||
<Text style={styles.statValue}>{item.sensor_data.temperature}°</Text>
|
||||
<Text style={styles.statLabel}>Temp</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
@ -116,11 +133,11 @@ export default function PatientsListScreen() {
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner fullScreen message="Loading patients..." />;
|
||||
return <LoadingSpinner fullScreen message="Loading beneficiaries..." />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <FullScreenError message={error} onRetry={() => loadPatients()} />;
|
||||
return <FullScreenError message={error} onRetry={() => loadBeneficiaries()} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -131,18 +148,18 @@ export default function PatientsListScreen() {
|
||||
<Text style={styles.greeting}>
|
||||
Hello, {user?.user_name || 'User'}
|
||||
</Text>
|
||||
<Text style={styles.headerTitle}>Your Patients</Text>
|
||||
<Text style={styles.headerTitle}>Beneficiaries</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.addButton}>
|
||||
<Ionicons name="add" size={24} color={AppColors.white} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Patient List */}
|
||||
{/* Beneficiary List */}
|
||||
<FlatList
|
||||
data={patients}
|
||||
data={beneficiaries}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={renderPatientCard}
|
||||
renderItem={renderBeneficiaryCard}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
@ -155,9 +172,9 @@ export default function PatientsListScreen() {
|
||||
ListEmptyComponent={
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="people-outline" size={64} color={AppColors.textMuted} />
|
||||
<Text style={styles.emptyTitle}>No patients yet</Text>
|
||||
<Text style={styles.emptyTitle}>No beneficiaries yet</Text>
|
||||
<Text style={styles.emptyText}>
|
||||
Add your first patient to start monitoring
|
||||
Add your first beneficiary to start monitoring
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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() {
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="(auth)" />
|
||||
<Stack.Screen name="(tabs)" />
|
||||
<Stack.Screen name="patients" />
|
||||
<Stack.Screen name="beneficiaries" />
|
||||
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
@ -54,7 +55,9 @@ function RootLayoutNav() {
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<RootLayoutNav />
|
||||
<BeneficiaryProvider>
|
||||
<RootLayoutNav />
|
||||
</BeneficiaryProvider>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
263
app/beneficiaries/[id]/dashboard.tsx
Normal file
263
app/beneficiaries/[id]/dashboard.tsx
Normal file
@ -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<WebView>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={styles.backButton} onPress={handleGoBack}>
|
||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>{beneficiaryName}</Text>
|
||||
<View style={styles.placeholder} />
|
||||
</View>
|
||||
<FullScreenError message={error} onRetry={handleRefresh} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={styles.backButton} onPress={handleGoBack}>
|
||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.headerCenter}>
|
||||
{currentBeneficiary && (
|
||||
<View style={styles.avatarSmall}>
|
||||
<Text style={styles.avatarText}>
|
||||
{currentBeneficiary.name.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View>
|
||||
<Text style={styles.headerTitle}>{beneficiaryName}</Text>
|
||||
{currentBeneficiary?.relationship && (
|
||||
<Text style={styles.headerSubtitle}>{currentBeneficiary.relationship}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{canGoBack && (
|
||||
<TouchableOpacity style={styles.actionButton} onPress={handleWebViewBack}>
|
||||
<Ionicons name="chevron-back" size={22} color={AppColors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity style={styles.actionButton} onPress={handleRefresh}>
|
||||
<Ionicons name="refresh" size={22} color={AppColors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* WebView */}
|
||||
<View style={styles.webViewContainer}>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={{ uri: DASHBOARD_URL }}
|
||||
style={styles.webView}
|
||||
onLoadStart={() => setIsLoading(true)}
|
||||
onLoadEnd={() => setIsLoading(false)}
|
||||
onError={handleError}
|
||||
onHttpError={handleError}
|
||||
onNavigationStateChange={handleNavigationStateChange}
|
||||
javaScriptEnabled={true}
|
||||
domStorageEnabled={true}
|
||||
startInLoadingState={true}
|
||||
scalesPageToFit={true}
|
||||
allowsBackForwardNavigationGestures={true}
|
||||
renderLoading={() => (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||
<Text style={styles.loadingText}>Loading dashboard...</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<View style={styles.loadingOverlay}>
|
||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Bottom Quick Actions */}
|
||||
<View style={styles.bottomBar}>
|
||||
<TouchableOpacity
|
||||
style={styles.quickAction}
|
||||
onPress={() => {
|
||||
if (currentBeneficiary) {
|
||||
setCurrentBeneficiary(currentBeneficiary);
|
||||
}
|
||||
router.push('/(tabs)/chat');
|
||||
}}
|
||||
>
|
||||
<Ionicons name="chatbubble-ellipses" size={24} color={AppColors.primary} />
|
||||
<Text style={styles.quickActionText}>Ask Julia</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.quickAction}
|
||||
onPress={() => router.push(`/beneficiaries/${id}`)}
|
||||
>
|
||||
<Ionicons name="person" size={24} color={AppColors.primary} />
|
||||
<Text style={styles.quickActionText}>Details</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
@ -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<Patient | null>(null);
|
||||
const { setCurrentBeneficiary } = useBeneficiary();
|
||||
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 <LoadingSpinner fullScreen message="Loading patient data..." />;
|
||||
return <LoadingSpinner fullScreen message="Loading beneficiary data..." />;
|
||||
}
|
||||
|
||||
if (error || !patient) {
|
||||
if (error || !beneficiary) {
|
||||
return (
|
||||
<FullScreenError
|
||||
message={error || 'Patient not found'}
|
||||
onRetry={() => loadPatient()}
|
||||
message={error || 'Beneficiary not found'}
|
||||
onRetry={() => loadBeneficiary()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -82,7 +89,7 @@ export default function PatientDashboardScreen() {
|
||||
>
|
||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>{patient.name}</Text>
|
||||
<Text style={styles.headerTitle}>{beneficiary.name}</Text>
|
||||
<TouchableOpacity style={styles.menuButton}>
|
||||
<Ionicons name="ellipsis-vertical" size={24} color={AppColors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
@ -99,67 +106,75 @@ export default function PatientDashboardScreen() {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{/* Patient Info Card */}
|
||||
{/* Beneficiary Info Card */}
|
||||
<View style={styles.infoCard}>
|
||||
<View style={styles.avatarContainer}>
|
||||
<Text style={styles.avatarText}>
|
||||
{patient.name.charAt(0).toUpperCase()}
|
||||
{beneficiary.name.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.statusBadge,
|
||||
patient.status === 'online' ? styles.onlineBadge : styles.offlineBadge,
|
||||
beneficiary.status === 'online' ? styles.onlineBadge : styles.offlineBadge,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.statusText}>
|
||||
{patient.status === 'online' ? 'Online' : 'Offline'}
|
||||
{beneficiary.status === 'online' ? 'Online' : 'Offline'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.patientName}>{patient.name}</Text>
|
||||
<Text style={styles.relationship}>{patient.relationship}</Text>
|
||||
<Text style={styles.beneficiaryName}>{beneficiary.name}</Text>
|
||||
<Text style={styles.relationship}>{beneficiary.relationship}</Text>
|
||||
<Text style={styles.lastSeen}>
|
||||
Last activity: {patient.last_activity}
|
||||
Last activity: {beneficiary.last_activity}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Health Stats */}
|
||||
{/* Sensor Stats */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Health Overview</Text>
|
||||
<Text style={styles.sectionTitle}>Sensor Overview</Text>
|
||||
|
||||
<View style={styles.statsGrid}>
|
||||
<View style={styles.statCard}>
|
||||
<View style={[styles.statIcon, { backgroundColor: '#FEE2E2' }]}>
|
||||
<Ionicons name="heart" size={24} color={AppColors.error} />
|
||||
<View style={[styles.statIcon, { backgroundColor: beneficiary.sensor_data?.motion_detected ? '#D1FAE5' : '#F3F4F6' }]}>
|
||||
<Ionicons
|
||||
name={beneficiary.sensor_data?.motion_detected ? "walk" : "walk-outline"}
|
||||
size={24}
|
||||
color={beneficiary.sensor_data?.motion_detected ? AppColors.success : AppColors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.statValue}>
|
||||
{patient.health_data?.heart_rate || '--'}
|
||||
{beneficiary.sensor_data?.motion_detected ? 'Active' : 'Inactive'}
|
||||
</Text>
|
||||
<Text style={styles.statLabel}>Heart Rate</Text>
|
||||
<Text style={styles.statUnit}>BPM</Text>
|
||||
<Text style={styles.statLabel}>Motion</Text>
|
||||
<Text style={styles.statUnit}>{beneficiary.sensor_data?.last_motion || '--'}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.statCard}>
|
||||
<View style={[styles.statIcon, { backgroundColor: '#DBEAFE' }]}>
|
||||
<Ionicons name="footsteps" size={24} color={AppColors.primary} />
|
||||
<View style={[styles.statIcon, { backgroundColor: beneficiary.sensor_data?.door_status === 'open' ? '#FEF3C7' : '#DBEAFE' }]}>
|
||||
<Ionicons
|
||||
name={beneficiary.sensor_data?.door_status === 'open' ? "enter-outline" : "home-outline"}
|
||||
size={24}
|
||||
color={beneficiary.sensor_data?.door_status === 'open' ? AppColors.warning : AppColors.primary}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.statValue}>
|
||||
{patient.health_data?.steps?.toLocaleString() || '--'}
|
||||
{beneficiary.sensor_data?.door_status === 'open' ? 'Open' : 'Closed'}
|
||||
</Text>
|
||||
<Text style={styles.statLabel}>Steps Today</Text>
|
||||
<Text style={styles.statUnit}>steps</Text>
|
||||
<Text style={styles.statLabel}>Door Status</Text>
|
||||
<Text style={styles.statUnit}>Main entrance</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.statCard}>
|
||||
<View style={[styles.statIcon, { backgroundColor: '#E0E7FF' }]}>
|
||||
<Ionicons name="moon" size={24} color={AppColors.primaryDark} />
|
||||
<Ionicons name="thermometer-outline" size={24} color={AppColors.primaryDark} />
|
||||
</View>
|
||||
<Text style={styles.statValue}>
|
||||
{patient.health_data?.sleep_hours || '--'}
|
||||
{beneficiary.sensor_data?.temperature || '--'}°C
|
||||
</Text>
|
||||
<Text style={styles.statLabel}>Sleep</Text>
|
||||
<Text style={styles.statUnit}>hours</Text>
|
||||
<Text style={styles.statLabel}>Temperature</Text>
|
||||
<Text style={styles.statUnit}>{beneficiary.sensor_data?.humidity || '--'}% humidity</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@ -192,9 +207,9 @@ export default function PatientDashboardScreen() {
|
||||
|
||||
<TouchableOpacity style={styles.actionCard}>
|
||||
<View style={[styles.actionIcon, { backgroundColor: '#F3E8FF' }]}>
|
||||
<Ionicons name="medkit" size={24} color="#9333EA" />
|
||||
<Ionicons name="analytics" size={24} color="#9333EA" />
|
||||
</View>
|
||||
<Text style={styles.actionLabel}>Medications</Text>
|
||||
<Text style={styles.actionLabel}>Activity Report</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@ -280,7 +295,7 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '500',
|
||||
color: AppColors.white,
|
||||
},
|
||||
patientName: {
|
||||
beneficiaryName: {
|
||||
fontSize: FontSizes['2xl'],
|
||||
fontWeight: '700',
|
||||
color: AppColors.textPrimary,
|
||||
@ -1,7 +1,7 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { AppColors } from '@/constants/theme';
|
||||
|
||||
export default function PatientsLayout() {
|
||||
export default function BeneficiariesLayout() {
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
@ -10,6 +10,7 @@ export default function PatientsLayout() {
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="[id]/index" />
|
||||
<Stack.Screen name="[id]/dashboard" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
86
contexts/BeneficiaryContext.tsx
Normal file
86
contexts/BeneficiaryContext.tsx
Normal file
@ -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<BeneficiaryContextType | undefined>(undefined);
|
||||
|
||||
export function BeneficiaryProvider({ children }: { children: React.ReactNode }) {
|
||||
const [currentBeneficiary, setCurrentBeneficiary] = useState<Beneficiary | null>(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 (
|
||||
<BeneficiaryContext.Provider
|
||||
value={{
|
||||
currentBeneficiary,
|
||||
setCurrentBeneficiary,
|
||||
clearCurrentBeneficiary,
|
||||
getBeneficiaryContext,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</BeneficiaryContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useBeneficiary() {
|
||||
const context = useContext(BeneficiaryContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useBeneficiary must be used within a BeneficiaryProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
15
package-lock.json
generated
15
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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<ApiResponse<{ patients: Patient[] }>> {
|
||||
// Beneficiaries (elderly people being monitored)
|
||||
async getBeneficiaries(): Promise<ApiResponse<{ beneficiaries: Beneficiary[] }>> {
|
||||
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<ApiResponse<Patient>> {
|
||||
const response = await this.getPatients();
|
||||
async getBeneficiary(id: number): Promise<ApiResponse<Beneficiary>> {
|
||||
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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user