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
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
title: 'Patients',
|
title: 'Home',
|
||||||
tabBarIcon: ({ color, size }) => (
|
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
|
<Tabs.Screen
|
||||||
name="chat"
|
name="chat"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@ -12,10 +12,12 @@ import {
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { api } from '@/services/api';
|
import { api } from '@/services/api';
|
||||||
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
||||||
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
||||||
import type { Message } from '@/types';
|
import type { Message } from '@/types';
|
||||||
|
|
||||||
export default function ChatScreen() {
|
export default function ChatScreen() {
|
||||||
|
const { currentBeneficiary, getBeneficiaryContext } = useBeneficiary();
|
||||||
const [messages, setMessages] = useState<Message[]>([
|
const [messages, setMessages] = useState<Message[]>([
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@ -44,7 +46,13 @@ export default function ChatScreen() {
|
|||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
|
|
||||||
try {
|
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) {
|
if (response.ok && response.data?.response) {
|
||||||
const assistantMessage: Message = {
|
const assistantMessage: Message = {
|
||||||
@ -74,7 +82,7 @@ export default function ChatScreen() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsSending(false);
|
setIsSending(false);
|
||||||
}
|
}
|
||||||
}, [input, isSending]);
|
}, [input, isSending, getBeneficiaryContext]);
|
||||||
|
|
||||||
const renderMessage = ({ item }: { item: Message }) => {
|
const renderMessage = ({ item }: { item: Message }) => {
|
||||||
const isUser = item.role === 'user';
|
const isUser = item.role === 'user';
|
||||||
@ -124,7 +132,11 @@ export default function ChatScreen() {
|
|||||||
<View>
|
<View>
|
||||||
<Text style={styles.headerTitle}>Julia AI</Text>
|
<Text style={styles.headerTitle}>Julia AI</Text>
|
||||||
<Text style={styles.headerSubtitle}>
|
<Text style={styles.headerSubtitle}>
|
||||||
{isSending ? 'Typing...' : 'Online'}
|
{isSending
|
||||||
|
? 'Typing...'
|
||||||
|
: currentBeneficiary
|
||||||
|
? `About ${currentBeneficiary.name}`
|
||||||
|
: 'Online'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</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 { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { api } from '@/services/api';
|
import { api } from '@/services/api';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
||||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||||
import { FullScreenError } from '@/components/ui/ErrorMessage';
|
import { FullScreenError } from '@/components/ui/ErrorMessage';
|
||||||
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
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 { user } = useAuth();
|
||||||
const [patients, setPatients] = useState<Patient[]>([]);
|
const { setCurrentBeneficiary } = useBeneficiary();
|
||||||
|
const [beneficiaries, setBeneficiaries] = useState<Beneficiary[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadPatients = useCallback(async (showLoading = true) => {
|
const loadBeneficiaries = useCallback(async (showLoading = true) => {
|
||||||
if (showLoading) setIsLoading(true);
|
if (showLoading) setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.getPatients();
|
const response = await api.getBeneficiaries();
|
||||||
|
|
||||||
if (response.ok && response.data) {
|
if (response.ok && response.data) {
|
||||||
setPatients(response.data.patients);
|
setBeneficiaries(response.data.beneficiaries);
|
||||||
} else {
|
} else {
|
||||||
setError(response.error?.message || 'Failed to load patients');
|
setError(response.error?.message || 'Failed to load beneficiaries');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
@ -45,25 +47,28 @@ export default function PatientsListScreen() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadPatients();
|
loadBeneficiaries();
|
||||||
}, [loadPatients]);
|
}, [loadBeneficiaries]);
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
loadPatients(false);
|
loadBeneficiaries(false);
|
||||||
}, [loadPatients]);
|
}, [loadBeneficiaries]);
|
||||||
|
|
||||||
const handlePatientPress = (patient: Patient) => {
|
const handleBeneficiaryPress = (beneficiary: Beneficiary) => {
|
||||||
router.push(`/patients/${patient.id}`);
|
// 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
|
<TouchableOpacity
|
||||||
style={styles.patientCard}
|
style={styles.beneficiaryCard}
|
||||||
onPress={() => handlePatientPress(item)}
|
onPress={() => handleBeneficiaryPress(item)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={styles.patientInfo}>
|
<View style={styles.beneficiaryInfo}>
|
||||||
<View style={styles.avatarContainer}>
|
<View style={styles.avatarContainer}>
|
||||||
<Text style={styles.avatarText}>
|
<Text style={styles.avatarText}>
|
||||||
{item.name.charAt(0).toUpperCase()}
|
{item.name.charAt(0).toUpperCase()}
|
||||||
@ -76,9 +81,9 @@ export default function PatientsListScreen() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.patientDetails}>
|
<View style={styles.beneficiaryDetails}>
|
||||||
<Text style={styles.patientName}>{item.name}</Text>
|
<Text style={styles.beneficiaryName}>{item.name}</Text>
|
||||||
<Text style={styles.patientRelationship}>{item.relationship}</Text>
|
<Text style={styles.beneficiaryRelationship}>{item.relationship}</Text>
|
||||||
<Text style={styles.lastActivity}>
|
<Text style={styles.lastActivity}>
|
||||||
<Ionicons name="time-outline" size={12} color={AppColors.textMuted} />{' '}
|
<Ionicons name="time-outline" size={12} color={AppColors.textMuted} />{' '}
|
||||||
{item.last_activity}
|
{item.last_activity}
|
||||||
@ -86,22 +91,34 @@ export default function PatientsListScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{item.health_data && (
|
{item.sensor_data && (
|
||||||
<View style={styles.healthStats}>
|
<View style={styles.sensorStats}>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
<Ionicons name="heart-outline" size={16} color={AppColors.error} />
|
<Ionicons
|
||||||
<Text style={styles.statValue}>{item.health_data.heart_rate}</Text>
|
name={item.sensor_data.motion_detected ? "walk" : "walk-outline"}
|
||||||
<Text style={styles.statLabel}>BPM</Text>
|
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>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
<Ionicons name="footsteps-outline" size={16} color={AppColors.primary} />
|
<Ionicons
|
||||||
<Text style={styles.statValue}>{item.health_data.steps?.toLocaleString()}</Text>
|
name={item.sensor_data.door_status === 'open' ? "enter-outline" : "home-outline"}
|
||||||
<Text style={styles.statLabel}>Steps</Text>
|
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>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
<Ionicons name="moon-outline" size={16} color={AppColors.primaryDark} />
|
<Ionicons name="thermometer-outline" size={16} color={AppColors.primaryDark} />
|
||||||
<Text style={styles.statValue}>{item.health_data.sleep_hours}h</Text>
|
<Text style={styles.statValue}>{item.sensor_data.temperature}°</Text>
|
||||||
<Text style={styles.statLabel}>Sleep</Text>
|
<Text style={styles.statLabel}>Temp</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@ -116,11 +133,11 @@ export default function PatientsListScreen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingSpinner fullScreen message="Loading patients..." />;
|
return <LoadingSpinner fullScreen message="Loading beneficiaries..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <FullScreenError message={error} onRetry={() => loadPatients()} />;
|
return <FullScreenError message={error} onRetry={() => loadBeneficiaries()} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -131,18 +148,18 @@ export default function PatientsListScreen() {
|
|||||||
<Text style={styles.greeting}>
|
<Text style={styles.greeting}>
|
||||||
Hello, {user?.user_name || 'User'}
|
Hello, {user?.user_name || 'User'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.headerTitle}>Your Patients</Text>
|
<Text style={styles.headerTitle}>Beneficiaries</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity style={styles.addButton}>
|
<TouchableOpacity style={styles.addButton}>
|
||||||
<Ionicons name="add" size={24} color={AppColors.white} />
|
<Ionicons name="add" size={24} color={AppColors.white} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Patient List */}
|
{/* Beneficiary List */}
|
||||||
<FlatList
|
<FlatList
|
||||||
data={patients}
|
data={beneficiaries}
|
||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
renderItem={renderPatientCard}
|
renderItem={renderBeneficiaryCard}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
@ -155,9 +172,9 @@ export default function PatientsListScreen() {
|
|||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View style={styles.emptyContainer}>
|
<View style={styles.emptyContainer}>
|
||||||
<Ionicons name="people-outline" size={64} color={AppColors.textMuted} />
|
<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}>
|
<Text style={styles.emptyText}>
|
||||||
Add your first patient to start monitoring
|
Add your first beneficiary to start monitoring
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
@ -201,7 +218,7 @@ const styles = StyleSheet.create({
|
|||||||
listContent: {
|
listContent: {
|
||||||
padding: Spacing.md,
|
padding: Spacing.md,
|
||||||
},
|
},
|
||||||
patientCard: {
|
beneficiaryCard: {
|
||||||
backgroundColor: AppColors.background,
|
backgroundColor: AppColors.background,
|
||||||
borderRadius: BorderRadius.lg,
|
borderRadius: BorderRadius.lg,
|
||||||
padding: Spacing.md,
|
padding: Spacing.md,
|
||||||
@ -212,7 +229,7 @@ const styles = StyleSheet.create({
|
|||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
},
|
},
|
||||||
patientInfo: {
|
beneficiaryInfo: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: Spacing.md,
|
marginBottom: Spacing.md,
|
||||||
@ -247,15 +264,15 @@ const styles = StyleSheet.create({
|
|||||||
offline: {
|
offline: {
|
||||||
backgroundColor: AppColors.offline,
|
backgroundColor: AppColors.offline,
|
||||||
},
|
},
|
||||||
patientDetails: {
|
beneficiaryDetails: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
patientName: {
|
beneficiaryName: {
|
||||||
fontSize: FontSizes.lg,
|
fontSize: FontSizes.lg,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
},
|
},
|
||||||
patientRelationship: {
|
beneficiaryRelationship: {
|
||||||
fontSize: FontSizes.sm,
|
fontSize: FontSizes.sm,
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.textSecondary,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
@ -265,7 +282,7 @@ const styles = StyleSheet.create({
|
|||||||
color: AppColors.textMuted,
|
color: AppColors.textMuted,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
healthStats: {
|
sensorStats: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-around',
|
||||||
paddingTop: Spacing.md,
|
paddingTop: Spacing.md,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import 'react-native-reanimated';
|
|||||||
|
|
||||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
import { useColorScheme } from '@/hooks/use-color-scheme';
|
||||||
import { AuthProvider, useAuth } from '@/contexts/AuthContext';
|
import { AuthProvider, useAuth } from '@/contexts/AuthContext';
|
||||||
|
import { BeneficiaryProvider } from '@/contexts/BeneficiaryContext';
|
||||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||||
|
|
||||||
// Prevent auto-hiding splash screen
|
// Prevent auto-hiding splash screen
|
||||||
@ -43,7 +44,7 @@ function RootLayoutNav() {
|
|||||||
<Stack screenOptions={{ headerShown: false }}>
|
<Stack screenOptions={{ headerShown: false }}>
|
||||||
<Stack.Screen name="(auth)" />
|
<Stack.Screen name="(auth)" />
|
||||||
<Stack.Screen name="(tabs)" />
|
<Stack.Screen name="(tabs)" />
|
||||||
<Stack.Screen name="patients" />
|
<Stack.Screen name="beneficiaries" />
|
||||||
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
|
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
@ -54,7 +55,9 @@ function RootLayoutNav() {
|
|||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<BeneficiaryProvider>
|
||||||
<RootLayoutNav />
|
<RootLayoutNav />
|
||||||
|
</BeneficiaryProvider>
|
||||||
</AuthProvider>
|
</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 { Ionicons } from '@expo/vector-icons';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { api } from '@/services/api';
|
import { api } from '@/services/api';
|
||||||
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
||||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||||
import { FullScreenError } from '@/components/ui/ErrorMessage';
|
import { FullScreenError } from '@/components/ui/ErrorMessage';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
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 { 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 [isLoading, setIsLoading] = useState(true);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadPatient = useCallback(async (showLoading = true) => {
|
const loadBeneficiary = useCallback(async (showLoading = true) => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
if (showLoading) setIsLoading(true);
|
if (showLoading) setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.getPatient(parseInt(id, 10));
|
const response = await api.getBeneficiary(parseInt(id, 10));
|
||||||
|
|
||||||
if (response.ok && response.data) {
|
if (response.ok && response.data) {
|
||||||
setPatient(response.data);
|
setBeneficiary(response.data);
|
||||||
} else {
|
} else {
|
||||||
setError(response.error?.message || 'Failed to load patient');
|
setError(response.error?.message || 'Failed to load beneficiary');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
@ -47,27 +49,32 @@ export default function PatientDashboardScreen() {
|
|||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadPatient();
|
loadBeneficiary();
|
||||||
}, [loadPatient]);
|
}, [loadBeneficiary]);
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
loadPatient(false);
|
loadBeneficiary(false);
|
||||||
}, [loadPatient]);
|
}, [loadBeneficiary]);
|
||||||
|
|
||||||
const handleChatPress = () => {
|
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');
|
router.push('/(tabs)/chat');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingSpinner fullScreen message="Loading patient data..." />;
|
return <LoadingSpinner fullScreen message="Loading beneficiary data..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !patient) {
|
if (error || !beneficiary) {
|
||||||
return (
|
return (
|
||||||
<FullScreenError
|
<FullScreenError
|
||||||
message={error || 'Patient not found'}
|
message={error || 'Beneficiary not found'}
|
||||||
onRetry={() => loadPatient()}
|
onRetry={() => loadBeneficiary()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -82,7 +89,7 @@ export default function PatientDashboardScreen() {
|
|||||||
>
|
>
|
||||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.headerTitle}>{patient.name}</Text>
|
<Text style={styles.headerTitle}>{beneficiary.name}</Text>
|
||||||
<TouchableOpacity style={styles.menuButton}>
|
<TouchableOpacity style={styles.menuButton}>
|
||||||
<Ionicons name="ellipsis-vertical" size={24} color={AppColors.textPrimary} />
|
<Ionicons name="ellipsis-vertical" size={24} color={AppColors.textPrimary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -99,67 +106,75 @@ export default function PatientDashboardScreen() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* Patient Info Card */}
|
{/* Beneficiary Info Card */}
|
||||||
<View style={styles.infoCard}>
|
<View style={styles.infoCard}>
|
||||||
<View style={styles.avatarContainer}>
|
<View style={styles.avatarContainer}>
|
||||||
<Text style={styles.avatarText}>
|
<Text style={styles.avatarText}>
|
||||||
{patient.name.charAt(0).toUpperCase()}
|
{beneficiary.name.charAt(0).toUpperCase()}
|
||||||
</Text>
|
</Text>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.statusBadge,
|
styles.statusBadge,
|
||||||
patient.status === 'online' ? styles.onlineBadge : styles.offlineBadge,
|
beneficiary.status === 'online' ? styles.onlineBadge : styles.offlineBadge,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text style={styles.statusText}>
|
<Text style={styles.statusText}>
|
||||||
{patient.status === 'online' ? 'Online' : 'Offline'}
|
{beneficiary.status === 'online' ? 'Online' : 'Offline'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.patientName}>{patient.name}</Text>
|
<Text style={styles.beneficiaryName}>{beneficiary.name}</Text>
|
||||||
<Text style={styles.relationship}>{patient.relationship}</Text>
|
<Text style={styles.relationship}>{beneficiary.relationship}</Text>
|
||||||
<Text style={styles.lastSeen}>
|
<Text style={styles.lastSeen}>
|
||||||
Last activity: {patient.last_activity}
|
Last activity: {beneficiary.last_activity}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Health Stats */}
|
{/* Sensor Stats */}
|
||||||
<View style={styles.section}>
|
<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.statsGrid}>
|
||||||
<View style={styles.statCard}>
|
<View style={styles.statCard}>
|
||||||
<View style={[styles.statIcon, { backgroundColor: '#FEE2E2' }]}>
|
<View style={[styles.statIcon, { backgroundColor: beneficiary.sensor_data?.motion_detected ? '#D1FAE5' : '#F3F4F6' }]}>
|
||||||
<Ionicons name="heart" size={24} color={AppColors.error} />
|
<Ionicons
|
||||||
|
name={beneficiary.sensor_data?.motion_detected ? "walk" : "walk-outline"}
|
||||||
|
size={24}
|
||||||
|
color={beneficiary.sensor_data?.motion_detected ? AppColors.success : AppColors.textMuted}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.statValue}>
|
<Text style={styles.statValue}>
|
||||||
{patient.health_data?.heart_rate || '--'}
|
{beneficiary.sensor_data?.motion_detected ? 'Active' : 'Inactive'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.statLabel}>Heart Rate</Text>
|
<Text style={styles.statLabel}>Motion</Text>
|
||||||
<Text style={styles.statUnit}>BPM</Text>
|
<Text style={styles.statUnit}>{beneficiary.sensor_data?.last_motion || '--'}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.statCard}>
|
<View style={styles.statCard}>
|
||||||
<View style={[styles.statIcon, { backgroundColor: '#DBEAFE' }]}>
|
<View style={[styles.statIcon, { backgroundColor: beneficiary.sensor_data?.door_status === 'open' ? '#FEF3C7' : '#DBEAFE' }]}>
|
||||||
<Ionicons name="footsteps" size={24} color={AppColors.primary} />
|
<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>
|
</View>
|
||||||
<Text style={styles.statValue}>
|
<Text style={styles.statValue}>
|
||||||
{patient.health_data?.steps?.toLocaleString() || '--'}
|
{beneficiary.sensor_data?.door_status === 'open' ? 'Open' : 'Closed'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.statLabel}>Steps Today</Text>
|
<Text style={styles.statLabel}>Door Status</Text>
|
||||||
<Text style={styles.statUnit}>steps</Text>
|
<Text style={styles.statUnit}>Main entrance</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.statCard}>
|
<View style={styles.statCard}>
|
||||||
<View style={[styles.statIcon, { backgroundColor: '#E0E7FF' }]}>
|
<View style={[styles.statIcon, { backgroundColor: '#E0E7FF' }]}>
|
||||||
<Ionicons name="moon" size={24} color={AppColors.primaryDark} />
|
<Ionicons name="thermometer-outline" size={24} color={AppColors.primaryDark} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.statValue}>
|
<Text style={styles.statValue}>
|
||||||
{patient.health_data?.sleep_hours || '--'}
|
{beneficiary.sensor_data?.temperature || '--'}°C
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.statLabel}>Sleep</Text>
|
<Text style={styles.statLabel}>Temperature</Text>
|
||||||
<Text style={styles.statUnit}>hours</Text>
|
<Text style={styles.statUnit}>{beneficiary.sensor_data?.humidity || '--'}% humidity</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -192,9 +207,9 @@ export default function PatientDashboardScreen() {
|
|||||||
|
|
||||||
<TouchableOpacity style={styles.actionCard}>
|
<TouchableOpacity style={styles.actionCard}>
|
||||||
<View style={[styles.actionIcon, { backgroundColor: '#F3E8FF' }]}>
|
<View style={[styles.actionIcon, { backgroundColor: '#F3E8FF' }]}>
|
||||||
<Ionicons name="medkit" size={24} color="#9333EA" />
|
<Ionicons name="analytics" size={24} color="#9333EA" />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.actionLabel}>Medications</Text>
|
<Text style={styles.actionLabel}>Activity Report</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -280,7 +295,7 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
color: AppColors.white,
|
color: AppColors.white,
|
||||||
},
|
},
|
||||||
patientName: {
|
beneficiaryName: {
|
||||||
fontSize: FontSizes['2xl'],
|
fontSize: FontSizes['2xl'],
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { AppColors } from '@/constants/theme';
|
import { AppColors } from '@/constants/theme';
|
||||||
|
|
||||||
export default function PatientsLayout() {
|
export default function BeneficiariesLayout() {
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
@ -10,6 +10,7 @@ export default function PatientsLayout() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name="[id]/index" />
|
<Stack.Screen name="[id]/index" />
|
||||||
|
<Stack.Screen name="[id]/dashboard" />
|
||||||
</Stack>
|
</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-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
|
"react-native-webview": "^13.16.0",
|
||||||
"react-native-worklets": "0.5.1"
|
"react-native-worklets": "0.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -10393,6 +10394,20 @@
|
|||||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/react-native-worklets": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
|
"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-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
|
"react-native-webview": "^13.16.0",
|
||||||
"react-native-worklets": "0.5.1"
|
"react-native-worklets": "0.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import * as SecureStore from 'expo-secure-store';
|
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 API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api';
|
||||||
const CLIENT_ID = 'MA_001';
|
const CLIENT_ID = 'MA_001';
|
||||||
@ -118,26 +118,28 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patients
|
// Beneficiaries (elderly people being monitored)
|
||||||
async getPatients(): Promise<ApiResponse<{ patients: Patient[] }>> {
|
async getBeneficiaries(): Promise<ApiResponse<{ beneficiaries: Beneficiary[] }>> {
|
||||||
const token = await this.getToken();
|
const token = await this.getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
|
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
|
// Replace with actual API call when available
|
||||||
const mockPatients: Patient[] = [
|
const mockBeneficiaries: Beneficiary[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Julia Smith',
|
name: 'Julia Smith',
|
||||||
status: 'online',
|
status: 'online',
|
||||||
relationship: 'Mother',
|
relationship: 'Mother',
|
||||||
last_activity: '2 min ago',
|
last_activity: '2 min ago',
|
||||||
health_data: {
|
sensor_data: {
|
||||||
heart_rate: 72,
|
motion_detected: true,
|
||||||
steps: 3450,
|
last_motion: '2 min ago',
|
||||||
sleep_hours: 7.5,
|
door_status: 'closed',
|
||||||
|
temperature: 22,
|
||||||
|
humidity: 45,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -146,29 +148,31 @@ class ApiService {
|
|||||||
status: 'offline',
|
status: 'offline',
|
||||||
relationship: 'Father',
|
relationship: 'Father',
|
||||||
last_activity: '1 hour ago',
|
last_activity: '1 hour ago',
|
||||||
health_data: {
|
sensor_data: {
|
||||||
heart_rate: 68,
|
motion_detected: false,
|
||||||
steps: 2100,
|
last_motion: '1 hour ago',
|
||||||
sleep_hours: 6.8,
|
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>> {
|
async getBeneficiary(id: number): Promise<ApiResponse<Beneficiary>> {
|
||||||
const response = await this.getPatients();
|
const response = await this.getBeneficiaries();
|
||||||
if (!response.ok || !response.data) {
|
if (!response.ok || !response.data) {
|
||||||
return { ok: false, error: response.error };
|
return { ok: false, error: response.error };
|
||||||
}
|
}
|
||||||
|
|
||||||
const patient = response.data.patients.find((p) => p.id === id);
|
const beneficiary = response.data.beneficiaries.find((b) => b.id === id);
|
||||||
if (!patient) {
|
if (!beneficiary) {
|
||||||
return { ok: false, error: { message: 'Patient not found', code: 'NOT_FOUND' } };
|
return { ok: false, error: { message: 'Beneficiary not found', code: 'NOT_FOUND' } };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data: patient, ok: true };
|
return { data: beneficiary, ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI Chat
|
// AI Chat
|
||||||
|
|||||||
@ -19,8 +19,8 @@ export interface LoginCredentials {
|
|||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patient Types
|
// Beneficiary Types (elderly people being monitored)
|
||||||
export interface Patient {
|
export interface Beneficiary {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
@ -28,13 +28,15 @@ export interface Patient {
|
|||||||
status: 'online' | 'offline';
|
status: 'online' | 'offline';
|
||||||
relationship?: string;
|
relationship?: string;
|
||||||
last_activity?: string;
|
last_activity?: string;
|
||||||
health_data?: HealthData;
|
sensor_data?: SensorData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HealthData {
|
export interface SensorData {
|
||||||
heart_rate?: number;
|
motion_detected?: boolean;
|
||||||
steps?: number;
|
last_motion?: string;
|
||||||
sleep_hours?: number;
|
door_status?: 'open' | 'closed';
|
||||||
|
temperature?: number;
|
||||||
|
humidity?: number;
|
||||||
last_updated?: string;
|
last_updated?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user