Sync all changes - profile restructure and scheme updates

- Restructured profile screens location
- Updated beneficiary detail page
- Updated API service
- Updated all scheme files (MainScheme, ENV API, Discussion, AppStore, SysAnal)
- Added PageHeader component

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sergei 2025-12-12 16:48:07 -08:00
parent 48384f07c5
commit abcc380984
17 changed files with 1290 additions and 686 deletions

View File

@ -125,45 +125,47 @@ export default function BeneficiaryDetailScreen() {
</View> </View>
<Text style={styles.beneficiaryName}>{beneficiary.name}</Text> <Text style={styles.beneficiaryName}>{beneficiary.name}</Text>
<Text style={styles.relationship}>{beneficiary.relationship}</Text> {beneficiary.address && (
<Text style={styles.relationship}>{beneficiary.address}</Text>
)}
<Text style={styles.lastSeen}> <Text style={styles.lastSeen}>
Last activity: {beneficiary.last_activity} {beneficiary.last_location ? `📍 ${beneficiary.last_location}` : ''} {beneficiary.last_activity || 'No recent activity'}
</Text> </Text>
</View> </View>
{/* Sensor Stats */} {/* Sensor Stats - using real API data */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Sensor 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: beneficiary.sensor_data?.motion_detected ? '#D1FAE5' : '#F3F4F6' }]}> <View style={[styles.statIcon, { backgroundColor: beneficiary.status === 'online' ? '#D1FAE5' : '#F3F4F6' }]}>
<Ionicons <Ionicons
name={beneficiary.sensor_data?.motion_detected ? "walk" : "walk-outline"} name={beneficiary.status === 'online' ? "walk" : "walk-outline"}
size={24} size={24}
color={beneficiary.sensor_data?.motion_detected ? AppColors.success : AppColors.textMuted} color={beneficiary.status === 'online' ? AppColors.success : AppColors.textMuted}
/> />
</View> </View>
<Text style={styles.statValue}> <Text style={styles.statValue}>
{beneficiary.sensor_data?.motion_detected ? 'Active' : 'Inactive'} {beneficiary.status === 'online' ? 'Active' : 'Inactive'}
</Text> </Text>
<Text style={styles.statLabel}>Motion</Text> <Text style={styles.statLabel}>Status</Text>
<Text style={styles.statUnit}>{beneficiary.sensor_data?.last_motion || '--'}</Text> <Text style={styles.statUnit}>{beneficiary.last_activity || '--'}</Text>
</View> </View>
<View style={styles.statCard}> <View style={styles.statCard}>
<View style={[styles.statIcon, { backgroundColor: beneficiary.sensor_data?.door_status === 'open' ? '#FEF3C7' : '#DBEAFE' }]}> <View style={[styles.statIcon, { backgroundColor: '#DBEAFE' }]}>
<Ionicons <Ionicons
name={beneficiary.sensor_data?.door_status === 'open' ? "enter-outline" : "home-outline"} name="location-outline"
size={24} size={24}
color={beneficiary.sensor_data?.door_status === 'open' ? AppColors.warning : AppColors.primary} color={AppColors.primary}
/> />
</View> </View>
<Text style={styles.statValue}> <Text style={styles.statValue} numberOfLines={1}>
{beneficiary.sensor_data?.door_status === 'open' ? 'Open' : 'Closed'} {beneficiary.last_location || '--'}
</Text> </Text>
<Text style={styles.statLabel}>Door Status</Text> <Text style={styles.statLabel}>Location</Text>
<Text style={styles.statUnit}>Main entrance</Text> <Text style={styles.statUnit} numberOfLines={1}>{beneficiary.before_last_location || '--'}</Text>
</View> </View>
<View style={styles.statCard}> <View style={styles.statCard}>
@ -171,10 +173,48 @@ export default function BeneficiaryDetailScreen() {
<Ionicons name="thermometer-outline" size={24} color={AppColors.primaryDark} /> <Ionicons name="thermometer-outline" size={24} color={AppColors.primaryDark} />
</View> </View>
<Text style={styles.statValue}> <Text style={styles.statValue}>
{beneficiary.sensor_data?.temperature || '--'}°C {beneficiary.temperature?.toFixed(1) || '--'}{beneficiary.units || '°F'}
</Text> </Text>
<Text style={styles.statLabel}>Temperature</Text> <Text style={styles.statLabel}>Temperature</Text>
<Text style={styles.statUnit}>{beneficiary.sensor_data?.humidity || '--'}% humidity</Text> <Text style={styles.statUnit}>Bedroom: {beneficiary.bedroom_temperature?.toFixed(1) || '--'}{beneficiary.units || '°F'}</Text>
</View>
</View>
{/* Additional stats row */}
<View style={[styles.statsGrid, { marginTop: Spacing.md }]}>
<View style={styles.statCard}>
<View style={[styles.statIcon, { backgroundColor: '#F3E8FF' }]}>
<Ionicons name="moon-outline" size={24} color="#9333EA" />
</View>
<Text style={styles.statValue}>
{beneficiary.sleep_hours?.toFixed(1) || '--'}h
</Text>
<Text style={styles.statLabel}>Sleep</Text>
</View>
<View style={styles.statCard}>
<View style={[styles.statIcon, { backgroundColor: beneficiary.wellness_score && beneficiary.wellness_score >= 70 ? '#D1FAE5' : '#FEF3C7' }]}>
<Ionicons
name="heart-outline"
size={24}
color={beneficiary.wellness_score && beneficiary.wellness_score >= 70 ? AppColors.success : AppColors.warning}
/>
</View>
<Text style={styles.statValue}>
{beneficiary.wellness_score || '--'}%
</Text>
<Text style={styles.statLabel}>Wellness</Text>
<Text style={styles.statUnit}>{beneficiary.wellness_descriptor || '--'}</Text>
</View>
<View style={styles.statCard}>
<View style={[styles.statIcon, { backgroundColor: '#DBEAFE' }]}>
<Ionicons name="home-outline" size={24} color={AppColors.primary} />
</View>
<Text style={styles.statValue} numberOfLines={1}>
{beneficiary.address?.split(',')[0] || '--'}
</Text>
<Text style={styles.statLabel}>Address</Text>
</View> </View>
</View> </View>
</View> </View>

View File

@ -0,0 +1,6 @@
import { Slot } from 'expo-router';
export default function ProfileLayout() {
// Using Slot instead of Stack to keep tab bar visible
return <Slot />;
}

View File

@ -11,6 +11,7 @@ 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 { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import { PageHeader } from '@/components/PageHeader';
interface InfoRowProps { interface InfoRowProps {
label: string; label: string;
@ -48,14 +49,16 @@ export default function AboutScreen() {
}; };
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<PageHeader title="About WellNuo" />
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
{/* App Logo & Name */} {/* App Logo & Name */}
<View style={styles.heroSection}> <View style={styles.heroSection}>
<View style={styles.logoContainer}> <Image
<Ionicons name="heart" size={48} color={AppColors.white} /> source={require('@/assets/images/icon.png')}
</View> style={styles.logoImage}
<Text style={styles.appName}>WellNuo</Text> resizeMode="contain"
/>
<Text style={styles.appTagline}>Caring for Those Who Matter Most</Text> <Text style={styles.appTagline}>Caring for Those Who Matter Most</Text>
</View> </View>
@ -156,32 +159,6 @@ export default function AboutScreen() {
title="Follow on Twitter" title="Follow on Twitter"
onPress={() => openURL('https://twitter.com/wellnuo')} onPress={() => openURL('https://twitter.com/wellnuo')}
/> />
<View style={styles.linkDivider} />
<LinkRow
icon="logo-github"
title="View on GitHub"
onPress={() => openURL('https://github.com/wellnuo')}
/>
</View>
</View>
{/* Acknowledgments */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Acknowledgments</Text>
<View style={styles.card}>
<Text style={styles.acknowledgment}>
WellNuo uses the following open-source software:
</Text>
<Text style={styles.license}> React Native (MIT License)</Text>
<Text style={styles.license}> Expo (MIT License)</Text>
<Text style={styles.license}> React Navigation (MIT License)</Text>
<Text style={styles.license}> And many other wonderful packages</Text>
<TouchableOpacity
style={styles.viewLicenses}
onPress={() => openURL('https://wellnuo.com/licenses')}
>
<Text style={styles.viewLicensesText}>View All Licenses</Text>
</TouchableOpacity>
</View> </View>
</View> </View>
@ -208,19 +185,10 @@ const styles = StyleSheet.create({
paddingVertical: Spacing.xl, paddingVertical: Spacing.xl,
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
}, },
logoContainer: { logoImage: {
width: 100, width: 180,
height: 100, height: 100,
borderRadius: 24, marginBottom: Spacing.sm,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.md,
},
appName: {
fontSize: 32,
fontWeight: '700',
color: AppColors.textPrimary,
}, },
appTagline: { appTagline: {
fontSize: FontSizes.base, fontSize: FontSizes.base,
@ -313,28 +281,6 @@ const styles = StyleSheet.create({
backgroundColor: AppColors.border, backgroundColor: AppColors.border,
marginLeft: Spacing.lg + 20 + Spacing.md, marginLeft: Spacing.lg + 20 + Spacing.md,
}, },
acknowledgment: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
},
license: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
paddingHorizontal: Spacing.lg,
paddingVertical: 2,
},
viewLicenses: {
marginTop: Spacing.md,
paddingVertical: Spacing.sm,
alignItems: 'center',
},
viewLicensesText: {
fontSize: FontSizes.sm,
color: AppColors.primary,
fontWeight: '500',
},
footer: { footer: {
alignItems: 'center', alignItems: 'center',
paddingVertical: Spacing.xl, paddingVertical: Spacing.xl,

View File

@ -14,6 +14,7 @@ import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router'; import { router } from 'expo-router';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { PageHeader } from '@/components/PageHeader';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
export default function EditProfileScreen() { export default function EditProfileScreen() {
@ -58,7 +59,8 @@ export default function EditProfileScreen() {
}; };
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<PageHeader title="Edit Profile" />
<KeyboardAvoidingView <KeyboardAvoidingView
style={styles.keyboardView} style={styles.keyboardView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}

View File

@ -11,6 +11,7 @@ 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 { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import { PageHeader } from '@/components/PageHeader';
interface FAQItemProps { interface FAQItemProps {
question: string; question: string;
@ -111,7 +112,8 @@ export default function HelpScreen() {
); );
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<PageHeader title="Help Center" />
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
{/* Search */} {/* Search */}
<View style={styles.searchSection}> <View style={styles.searchSection}>

View File

@ -57,7 +57,6 @@ export default function ProfileScreen() {
// Settings states // Settings states
const [pushNotifications, setPushNotifications] = useState(true); const [pushNotifications, setPushNotifications] = useState(true);
const [emailNotifications, setEmailNotifications] = useState(false); const [emailNotifications, setEmailNotifications] = useState(false);
const [darkMode, setDarkMode] = useState(false);
const [biometricLogin, setBiometricLogin] = useState(false); const [biometricLogin, setBiometricLogin] = useState(false);
const handleLogout = () => { const handleLogout = () => {
@ -80,17 +79,17 @@ export default function ProfileScreen() {
}; };
// Navigation handlers - now using actual page navigation // Navigation handlers - now using actual page navigation
const handleEditProfile = () => router.push('/profile/edit'); const handleEditProfile = () => router.push('/(tabs)/profile/edit');
const handleNotifications = () => router.push('/profile/notifications'); const handleNotifications = () => router.push('/(tabs)/profile/notifications');
const handlePrivacy = () => router.push('/profile/privacy'); const handlePrivacy = () => router.push('/(tabs)/profile/privacy');
const handleUpgrade = () => router.push('/profile/subscription'); const handleUpgrade = () => router.push('/(tabs)/profile/subscription');
const handlePayment = () => router.push('/profile/subscription'); const handlePayment = () => router.push('/(tabs)/profile/subscription');
const handleHelp = () => router.push('/profile/help'); const handleHelp = () => router.push('/(tabs)/profile/help');
const handleSupport = () => router.push('/profile/support'); const handleSupport = () => router.push('/(tabs)/profile/support');
const handleTerms = () => router.push('/profile/terms'); const handleTerms = () => router.push('/(tabs)/profile/terms');
const handlePrivacyPolicy = () => router.push('/profile/privacy-policy'); const handlePrivacyPolicy = () => router.push('/(tabs)/profile/privacy-policy');
const handleLanguage = () => router.push('/profile/language'); const handleLanguage = () => router.push('/(tabs)/profile/language');
const handleAbout = () => router.push('/profile/about'); const handleAbout = () => router.push('/(tabs)/profile/about');
const handleDevInfo = () => { const handleDevInfo = () => {
Alert.alert( Alert.alert(
@ -221,27 +220,6 @@ export default function ProfileScreen() {
} }
/> />
<View style={styles.menuDivider} /> <View style={styles.menuDivider} />
<MenuItem
icon="moon-outline"
iconBgColor="#E0E7FF"
iconColor="#4F46E5"
title="Dark Mode"
subtitle="Coming soon"
showChevron={false}
rightElement={
<Switch
value={darkMode}
onValueChange={(value) => {
setDarkMode(value);
Alert.alert('Dark Mode', 'Dark mode will be available in a future update!');
setDarkMode(false);
}}
trackColor={{ false: '#E5E7EB', true: AppColors.primaryLight }}
thumbColor={darkMode ? AppColors.primary : '#9CA3AF'}
/>
}
/>
<View style={styles.menuDivider} />
<MenuItem <MenuItem
icon="finger-print" icon="finger-print"
iconBgColor="#D1FAE5" iconBgColor="#D1FAE5"

View File

@ -11,6 +11,7 @@ import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router'; import { router } from 'expo-router';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import { PageHeader } from '@/components/PageHeader';
interface LanguageOptionProps { interface LanguageOptionProps {
code: string; code: string;
@ -111,7 +112,8 @@ export default function LanguageScreen() {
const comingSoonLanguages = languages.filter(l => !l.available); const comingSoonLanguages = languages.filter(l => !l.available);
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<PageHeader title="Language" />
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
{/* Current Language */} {/* Current Language */}
<View style={styles.section}> <View style={styles.section}>

View File

@ -12,6 +12,7 @@ import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router'; import { router } from 'expo-router';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import { PageHeader } from '@/components/PageHeader';
interface NotificationSettingProps { interface NotificationSettingProps {
icon: keyof typeof Ionicons.glyphMap; icon: keyof typeof Ionicons.glyphMap;
@ -89,7 +90,8 @@ export default function NotificationsScreen() {
}; };
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<PageHeader title="Notifications" />
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
{/* Alert Types */} {/* Alert Types */}
<View style={styles.section}> <View style={styles.section}>

View File

@ -8,6 +8,7 @@ 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 { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import { PageHeader } from '@/components/PageHeader';
interface PrivacyHighlightProps { interface PrivacyHighlightProps {
icon: keyof typeof Ionicons.glyphMap; icon: keyof typeof Ionicons.glyphMap;
@ -39,7 +40,8 @@ function PrivacyHighlight({
export default function PrivacyPolicyScreen() { export default function PrivacyPolicyScreen() {
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<PageHeader title="Privacy Policy" />
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
{/* Highlights */} {/* Highlights */}
<View style={styles.highlightsSection}> <View style={styles.highlightsSection}>

View File

@ -15,6 +15,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router'; import { router } from 'expo-router';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import { PageHeader } from '@/components/PageHeader';
interface SecurityItemProps { interface SecurityItemProps {
icon: keyof typeof Ionicons.glyphMap; icon: keyof typeof Ionicons.glyphMap;
@ -193,7 +194,8 @@ export default function PrivacyScreen() {
}; };
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<PageHeader title="Privacy & Security" />
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
{/* Password & Authentication */} {/* Password & Authentication */}
<View style={styles.section}> <View style={styles.section}>

View File

@ -11,6 +11,7 @@ import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router'; import { router } from 'expo-router';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import { PageHeader } from '@/components/PageHeader';
interface PlanFeatureProps { interface PlanFeatureProps {
text: string; text: string;
@ -64,7 +65,8 @@ export default function SubscriptionScreen() {
}; };
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<PageHeader title="WellNuo Pro" />
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
{/* Current Plan Badge */} {/* Current Plan Badge */}
<View style={styles.currentPlanBanner}> <View style={styles.currentPlanBanner}>

File diff suppressed because it is too large Load Diff

View File

@ -7,10 +7,12 @@ import {
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import { PageHeader } from '@/components/PageHeader';
export default function TermsScreen() { export default function TermsScreen() {
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<PageHeader title="Terms of Service" />
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
<View style={styles.content}> <View style={styles.content}>
<Text style={styles.lastUpdated}>Last Updated: December 2024</Text> <Text style={styles.lastUpdated}>Last Updated: December 2024</Text>

View File

@ -1,81 +0,0 @@
import { Stack } from 'expo-router';
import { AppColors } from '@/constants/theme';
export default function ProfileLayout() {
return (
<Stack
screenOptions={{
headerStyle: {
backgroundColor: AppColors.background,
},
headerTintColor: AppColors.primary,
headerTitleStyle: {
fontWeight: '600',
},
headerBackTitleVisible: false,
headerShadowVisible: false,
}}
>
<Stack.Screen
name="edit"
options={{
title: 'Edit Profile',
}}
/>
<Stack.Screen
name="notifications"
options={{
title: 'Notifications',
}}
/>
<Stack.Screen
name="privacy"
options={{
title: 'Privacy & Security',
}}
/>
<Stack.Screen
name="help"
options={{
title: 'Help Center',
}}
/>
<Stack.Screen
name="support"
options={{
title: 'Contact Support',
}}
/>
<Stack.Screen
name="subscription"
options={{
title: 'WellNuo Pro',
}}
/>
<Stack.Screen
name="terms"
options={{
title: 'Terms of Service',
}}
/>
<Stack.Screen
name="privacy-policy"
options={{
title: 'Privacy Policy',
}}
/>
<Stack.Screen
name="about"
options={{
title: 'About WellNuo',
}}
/>
<Stack.Screen
name="language"
options={{
title: 'Language',
}}
/>
</Stack>
);
}

View File

@ -1,475 +0,0 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
TextInput,
Linking,
Alert,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
interface ContactMethodProps {
icon: keyof typeof Ionicons.glyphMap;
iconColor: string;
iconBgColor: string;
title: string;
subtitle: string;
onPress: () => void;
}
function ContactMethod({
icon,
iconColor,
iconBgColor,
title,
subtitle,
onPress,
}: ContactMethodProps) {
return (
<TouchableOpacity style={styles.contactMethod} onPress={onPress}>
<View style={[styles.contactIcon, { backgroundColor: iconBgColor }]}>
<Ionicons name={icon} size={24} color={iconColor} />
</View>
<View style={styles.contactContent}>
<Text style={styles.contactTitle}>{title}</Text>
<Text style={styles.contactSubtitle}>{subtitle}</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
</TouchableOpacity>
);
}
export default function SupportScreen() {
const [subject, setSubject] = useState('');
const [message, setMessage] = useState('');
const [category, setCategory] = useState('');
const [isSending, setIsSending] = useState(false);
const categories = [
'Technical Issue',
'Billing Question',
'Feature Request',
'Account Help',
'Emergency',
'Other',
];
const handleCall = () => {
Linking.openURL('tel:+15551234567').catch(() => {
Alert.alert('Error', 'Unable to make phone call');
});
};
const handleEmail = () => {
Linking.openURL('mailto:support@wellnuo.com?subject=Support Request').catch(() => {
Alert.alert('Error', 'Unable to open email client');
});
};
const handleChat = () => {
Alert.alert(
'Live Chat',
'Connecting to a support agent...\n\nEstimated wait time: 2 minutes',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Start Chat', onPress: () => Alert.alert('Coming Soon', 'Live chat feature coming soon!') },
]
);
};
const handleSendTicket = async () => {
if (!category) {
Alert.alert('Error', 'Please select a category');
return;
}
if (!subject.trim()) {
Alert.alert('Error', 'Please enter a subject');
return;
}
if (!message.trim()) {
Alert.alert('Error', 'Please describe your issue');
return;
}
setIsSending(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
setIsSending(false);
Alert.alert(
'Ticket Submitted',
'Thank you for contacting us!\n\nTicket #WN-2024-12345\n\nWe\'ll respond within 24 hours.',
[{ text: 'OK', onPress: () => router.back() }]
);
};
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<KeyboardAvoidingView
style={styles.keyboardView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Quick Contact */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Quick Contact</Text>
<View style={styles.card}>
<ContactMethod
icon="call"
iconColor="#10B981"
iconBgColor="#D1FAE5"
title="Call Us"
subtitle="+1 (555) 123-4567"
onPress={handleCall}
/>
<View style={styles.divider} />
<ContactMethod
icon="mail"
iconColor="#3B82F6"
iconBgColor="#DBEAFE"
title="Email Support"
subtitle="support@wellnuo.com"
onPress={handleEmail}
/>
<View style={styles.divider} />
<ContactMethod
icon="chatbubbles"
iconColor="#8B5CF6"
iconBgColor="#EDE9FE"
title="Live Chat"
subtitle="Available 24/7"
onPress={handleChat}
/>
</View>
</View>
{/* Support Hours */}
<View style={styles.section}>
<View style={styles.hoursCard}>
<Ionicons name="time" size={24} color={AppColors.primary} />
<View style={styles.hoursContent}>
<Text style={styles.hoursTitle}>Support Hours</Text>
<Text style={styles.hoursText}>Phone: Mon-Fri 8am-8pm EST</Text>
<Text style={styles.hoursText}>Email & Chat: 24/7</Text>
<Text style={styles.hoursText}>Emergency: 24/7</Text>
</View>
</View>
</View>
{/* Submit a Ticket */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Submit a Ticket</Text>
<View style={styles.formCard}>
{/* Category */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Category *</Text>
<View style={styles.categoryContainer}>
{categories.map((cat) => (
<TouchableOpacity
key={cat}
style={[
styles.categoryChip,
category === cat && styles.categoryChipSelected,
]}
onPress={() => setCategory(cat)}
>
<Text
style={[
styles.categoryChipText,
category === cat && styles.categoryChipTextSelected,
]}
>
{cat}
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Subject */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Subject *</Text>
<TextInput
style={styles.input}
value={subject}
onChangeText={setSubject}
placeholder="Brief description of your issue"
placeholderTextColor={AppColors.textMuted}
/>
</View>
{/* Message */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Message *</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={message}
onChangeText={setMessage}
placeholder="Please describe your issue in detail. Include any error messages or steps to reproduce the problem."
placeholderTextColor={AppColors.textMuted}
multiline
numberOfLines={5}
textAlignVertical="top"
/>
</View>
{/* Attachments hint */}
<View style={styles.attachmentHint}>
<Ionicons name="attach" size={16} color={AppColors.textMuted} />
<Text style={styles.attachmentHintText}>
Need to attach screenshots? Reply to your ticket email.
</Text>
</View>
{/* Submit Button */}
<TouchableOpacity
style={[styles.submitButton, isSending && styles.submitButtonDisabled]}
onPress={handleSendTicket}
disabled={isSending}
>
<Text style={styles.submitButtonText}>
{isSending ? 'Sending...' : 'Submit Ticket'}
</Text>
</TouchableOpacity>
</View>
</View>
{/* FAQ Link */}
<View style={styles.section}>
<TouchableOpacity
style={styles.faqLink}
onPress={() => router.push('/profile/help')}
>
<View style={styles.faqLinkContent}>
<Ionicons name="help-circle" size={24} color={AppColors.primary} />
<View style={styles.faqLinkText}>
<Text style={styles.faqLinkTitle}>Check our Help Center</Text>
<Text style={styles.faqLinkSubtitle}>
Find answers to common questions
</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
</TouchableOpacity>
</View>
{/* Emergency Notice */}
<View style={styles.emergencyNotice}>
<Ionicons name="warning" size={20} color={AppColors.error} />
<Text style={styles.emergencyText}>
If you're experiencing a medical emergency, please call 911 or your local
emergency services immediately.
</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
keyboardView: {
flex: 1,
},
section: {
marginTop: Spacing.md,
},
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textSecondary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
textTransform: 'uppercase',
},
card: {
backgroundColor: AppColors.background,
},
contactMethod: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
},
contactIcon: {
width: 48,
height: 48,
borderRadius: BorderRadius.md,
justifyContent: 'center',
alignItems: 'center',
},
contactContent: {
flex: 1,
marginLeft: Spacing.md,
},
contactTitle: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
contactSubtitle: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginTop: 2,
},
divider: {
height: 1,
backgroundColor: AppColors.border,
marginLeft: Spacing.lg + 48 + Spacing.md,
},
hoursCard: {
flexDirection: 'row',
backgroundColor: AppColors.background,
marginHorizontal: Spacing.lg,
padding: Spacing.md,
borderRadius: BorderRadius.lg,
alignItems: 'flex-start',
},
hoursContent: {
flex: 1,
marginLeft: Spacing.md,
},
hoursTitle: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
hoursText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginTop: 2,
},
formCard: {
backgroundColor: AppColors.background,
padding: Spacing.lg,
},
inputGroup: {
marginBottom: Spacing.md,
},
label: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
input: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.md,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.md,
fontSize: FontSizes.base,
color: AppColors.textPrimary,
borderWidth: 1,
borderColor: AppColors.border,
},
textArea: {
minHeight: 120,
paddingTop: Spacing.md,
},
categoryContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: Spacing.xs,
},
categoryChip: {
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.xs,
borderRadius: BorderRadius.full,
backgroundColor: AppColors.surface,
marginRight: Spacing.xs,
marginBottom: Spacing.xs,
borderWidth: 1,
borderColor: AppColors.border,
},
categoryChipSelected: {
backgroundColor: AppColors.primary,
borderColor: AppColors.primary,
},
categoryChipText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
categoryChipTextSelected: {
color: AppColors.white,
},
attachmentHint: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: Spacing.lg,
},
attachmentHintText: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginLeft: Spacing.xs,
},
submitButton: {
backgroundColor: AppColors.primary,
borderRadius: BorderRadius.lg,
paddingVertical: Spacing.md,
alignItems: 'center',
},
submitButtonDisabled: {
opacity: 0.6,
},
submitButtonText: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.white,
},
faqLink: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.background,
marginHorizontal: Spacing.lg,
padding: Spacing.md,
borderRadius: BorderRadius.lg,
},
faqLinkContent: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
faqLinkText: {
marginLeft: Spacing.md,
},
faqLinkTitle: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
faqLinkSubtitle: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
emergencyNotice: {
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: '#FEE2E2',
marginHorizontal: Spacing.lg,
marginVertical: Spacing.lg,
padding: Spacing.md,
borderRadius: BorderRadius.lg,
},
emergencyText: {
flex: 1,
fontSize: FontSizes.xs,
color: AppColors.error,
marginLeft: Spacing.sm,
lineHeight: 18,
},
});

66
components/PageHeader.tsx Normal file
View File

@ -0,0 +1,66 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import { AppColors, FontSizes, Spacing } from '@/constants/theme';
interface PageHeaderProps {
title: string;
onBack?: () => void;
rightElement?: React.ReactNode;
}
export function PageHeader({ title, onBack, rightElement }: PageHeaderProps) {
const handleBack = () => {
if (onBack) {
onBack();
} else {
router.back();
}
};
return (
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={handleBack}>
<Ionicons name="chevron-back" size={28} color={AppColors.primary} />
</TouchableOpacity>
<Text style={styles.title}>{title}</Text>
<View style={styles.rightContainer}>
{rightElement || <View style={styles.placeholder} />}
</View>
</View>
);
}
const styles = StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: Spacing.sm,
paddingVertical: Spacing.md,
backgroundColor: AppColors.background,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
backButton: {
width: 44,
height: 44,
justifyContent: 'center',
alignItems: 'center',
},
title: {
flex: 1,
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
textAlign: 'center',
},
rightContainer: {
width: 44,
alignItems: 'flex-end',
},
placeholder: {
width: 44,
},
});

View File

@ -4,6 +4,22 @@ import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError, Da
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';
// Avatar images for elderly beneficiaries (stable Unsplash URLs)
const ELDERLY_AVATARS = [
'https://images.unsplash.com/photo-1559839734-2b71ea197ec2?w=200&h=200&fit=crop&crop=face', // elderly woman 1
'https://images.unsplash.com/photo-1581579438747-104c53d7fbc4?w=200&h=200&fit=crop&crop=face', // elderly man 1
'https://images.unsplash.com/photo-1566616213894-2d4e1baee5d8?w=200&h=200&fit=crop&crop=face', // elderly woman 2
'https://images.unsplash.com/photo-1552058544-f2b08422138a?w=200&h=200&fit=crop&crop=face', // elderly man 2
'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=200&h=200&fit=crop&crop=face', // elderly woman 3
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200&h=200&fit=crop&crop=face', // elderly man 3
];
// Get consistent avatar based on deployment_id
function getAvatarForBeneficiary(deploymentId: number): string {
const index = deploymentId % ELDERLY_AVATARS.length;
return ELDERLY_AVATARS[index];
}
// Helper function to format time ago // Helper function to format time ago
function formatTimeAgo(date: Date): string { function formatTimeAgo(date: Date): string {
const now = new Date(); const now = new Date();
@ -176,15 +192,39 @@ class ApiService {
} }
async getBeneficiary(id: number): Promise<ApiResponse<Beneficiary>> { async getBeneficiary(id: number): Promise<ApiResponse<Beneficiary>> {
const response = await this.getBeneficiaries(); // Use real API data via getPatientDashboard
const response = await this.getPatientDashboard(id.toString());
if (!response.ok || !response.data) { if (!response.ok || !response.data) {
return { ok: false, error: response.error }; return { ok: false, error: response.error || { message: 'Beneficiary not found', code: 'NOT_FOUND' } };
} }
const beneficiary = response.data.beneficiaries.find((b) => b.id === id); const data = response.data;
if (!beneficiary) { // Determine if patient is "online" based on last_detected_time
return { ok: false, error: { message: 'Beneficiary not found', code: 'NOT_FOUND' } }; const lastDetected = data.last_detected_time ? new Date(data.last_detected_time) : null;
} const isRecent = lastDetected && (Date.now() - lastDetected.getTime()) < 30 * 60 * 1000; // 30 min
const deploymentId = parseInt(data.deployment_id, 10);
const beneficiary: Beneficiary = {
id: deploymentId,
name: data.name,
avatar: getAvatarForBeneficiary(deploymentId),
status: isRecent ? 'online' : 'offline',
address: data.address,
timezone: data.time_zone,
wellness_score: data.wellness_score_percent,
wellness_descriptor: data.wellness_descriptor,
last_location: data.last_location,
temperature: data.temperature,
units: data.units,
sleep_hours: data.sleep_hours,
bedroom_temperature: data.bedroom_temperature,
before_last_location: data.before_last_location,
last_detected_time: data.last_detected_time,
last_activity: data.last_detected_time
? formatTimeAgo(new Date(data.last_detected_time))
: undefined,
};
return { data: beneficiary, ok: true }; return { data: beneficiary, ok: true };
} }