Full sync with auth screens and discussion docs
Added: - forgot-password.tsx, register.tsx auth screens - Discussion_Points.md and Discussion_Points.txt Updated: - login, chat, index, beneficiary detail screens - profile/help and profile/support - API service - Scheme files (Discussion, AppStore) All assets/images are tracked and safe. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
abcc380984
commit
1e2c2b9856
66
Discussion_Points.md
Normal file
66
Discussion_Points.md
Normal file
@ -0,0 +1,66 @@
|
||||
# WellNuo Mobile App
|
||||
## Discussion Points for EluxNetworks Team
|
||||
|
||||
---
|
||||
|
||||
## 1. Backend/API Development Access
|
||||
|
||||
**Request:** Can I get access to develop backend/API endpoints myself?
|
||||
|
||||
### Why?
|
||||
|
||||
| Problem | Impact |
|
||||
|---------|--------|
|
||||
| No fixed technical specification | Requirements will change frequently |
|
||||
| Waiting for backend = blocked work | Significant development delays |
|
||||
| Can't predict all API needs upfront | Discovery happens during implementation |
|
||||
|
||||
### Option A: Access Granted (Preferred)
|
||||
|
||||
**I need:**
|
||||
- Repository access (or separate repo)
|
||||
- Database documentation
|
||||
- Architecture overview
|
||||
|
||||
**I will:**
|
||||
- Implement endpoints myself
|
||||
- Document all changes
|
||||
|
||||
### Option B: No Access
|
||||
|
||||
**EluxNetworks implements these APIs:**
|
||||
|
||||
| Category | Endpoints |
|
||||
|----------|-----------|
|
||||
| Auth | Registration, Password reset, Email verification |
|
||||
| Billing | Subscription plans, Payments (Stripe) |
|
||||
| Beneficiary | CRUD, Family invites |
|
||||
| Orders | Product catalog, Order tracking |
|
||||
| Notifications | Device registration, Push settings |
|
||||
| Profile | User CRUD, Password change, Account deletion |
|
||||
|
||||
**Important:** This list is a rough estimate, not a final specification. Actual requirements will change during development. Expect multiple rounds of revisions and delays.
|
||||
|
||||
---
|
||||
|
||||
## 2. WebView Embed Mode
|
||||
|
||||
**Request:** Add `?embedded=true` parameter to hide UI elements
|
||||
|
||||
**Hide:**
|
||||
- Header "Dashboard Details"
|
||||
- Navigation arrows (< >)
|
||||
- Logout button
|
||||
|
||||
**Reason:** Mobile app has its own navigation and auth.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Option | Result |
|
||||
|--------|--------|
|
||||
| **Backend access** | Fast development, flexible, minimal delays |
|
||||
| **Wait for APIs** | Slow, multiple spec revisions, blocked work |
|
||||
|
||||
**Recommendation:** Backend access for fastest delivery.
|
||||
67
Discussion_Points.txt
Normal file
67
Discussion_Points.txt
Normal file
@ -0,0 +1,67 @@
|
||||
WELLNUO MOBILE APP
|
||||
Discussion Points for EluxNetworks Team
|
||||
|
||||
────────────────────────────────────────────────────────────
|
||||
|
||||
1. BACKEND/API DEVELOPMENT ACCESS
|
||||
|
||||
Request: Can I get access to develop backend/API endpoints myself?
|
||||
|
||||
|
||||
Why?
|
||||
|
||||
- No fixed technical specification - requirements will change frequently
|
||||
- Waiting for backend = blocked frontend work = significant delays
|
||||
- Can't predict all API needs upfront - discovery happens during implementation
|
||||
|
||||
|
||||
Option A: Access Granted (Preferred)
|
||||
|
||||
I need:
|
||||
- Repository access (or separate repo)
|
||||
- Database documentation
|
||||
- Architecture overview
|
||||
|
||||
I will:
|
||||
- Implement endpoints myself
|
||||
- Document all changes
|
||||
|
||||
|
||||
Option B: No Access
|
||||
|
||||
EluxNetworks implements these APIs:
|
||||
|
||||
- Auth: Registration, Password reset, Email verification
|
||||
- Billing: Subscription plans, Payments (Stripe)
|
||||
- Beneficiary: CRUD, Family invites
|
||||
- Orders: Product catalog, Order tracking
|
||||
- Notifications: Device registration, Push settings
|
||||
- Profile: User CRUD, Password change, Account deletion
|
||||
|
||||
Important: This list is a rough estimate, not a final specification.
|
||||
Actual requirements will change during development.
|
||||
Expect multiple rounds of revisions and delays.
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────────
|
||||
|
||||
2. WEBVIEW EMBED MODE
|
||||
|
||||
Request: Add ?embedded=true parameter to hide UI elements
|
||||
|
||||
Hide:
|
||||
- Header "Dashboard Details"
|
||||
- Navigation arrows (< >)
|
||||
- Logout button
|
||||
|
||||
Reason: Mobile app has its own navigation and auth.
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────────
|
||||
|
||||
SUMMARY
|
||||
|
||||
Backend access = fast development, flexible, minimal delays
|
||||
Wait for APIs = slow, multiple spec revisions, blocked work
|
||||
|
||||
Recommendation: Backend access for fastest delivery.
|
||||
159
app/(auth)/forgot-password.tsx
Normal file
159
app/(auth)/forgot-password.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
||||
|
||||
export default function ForgotPasswordScreen() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!email.trim()) {
|
||||
Alert.alert('Error', 'Please enter your email address');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show "in development" message
|
||||
Alert.alert(
|
||||
'Coming Soon',
|
||||
'Password recovery is currently under development. Please contact support for assistance.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Back Button */}
|
||||
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Ionicons name="lock-open-outline" size={48} color={AppColors.primary} />
|
||||
</View>
|
||||
<Text style={styles.title}>Forgot Password?</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Enter your email address and we'll send you instructions to reset your password.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Form */}
|
||||
<View style={styles.form}>
|
||||
<Input
|
||||
label="Email Address"
|
||||
placeholder="Enter your email"
|
||||
leftIcon="mail-outline"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title="Send Reset Link"
|
||||
onPress={handleSubmit}
|
||||
loading={isLoading}
|
||||
fullWidth
|
||||
size="lg"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Remember your password? </Text>
|
||||
<TouchableOpacity onPress={() => router.back()}>
|
||||
<Text style={styles.footerLink}>Sign In</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: AppColors.background,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: Spacing.lg,
|
||||
paddingTop: Spacing.xl,
|
||||
paddingBottom: Spacing.xl,
|
||||
},
|
||||
backButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: BorderRadius.full,
|
||||
backgroundColor: AppColors.surface,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: Spacing.lg,
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginBottom: Spacing.xl,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: BorderRadius.full,
|
||||
backgroundColor: AppColors.primaryLight + '30',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: Spacing.lg,
|
||||
},
|
||||
title: {
|
||||
fontSize: FontSizes['2xl'],
|
||||
fontWeight: '700',
|
||||
color: AppColors.textPrimary,
|
||||
marginBottom: Spacing.sm,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.textSecondary,
|
||||
textAlign: 'center',
|
||||
lineHeight: 22,
|
||||
},
|
||||
form: {
|
||||
marginBottom: Spacing.xl,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.textSecondary,
|
||||
},
|
||||
footerLink: {
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
@ -108,7 +108,7 @@ export default function LoginScreen() {
|
||||
returnKeyType="done"
|
||||
/>
|
||||
|
||||
<TouchableOpacity style={styles.forgotPassword}>
|
||||
<TouchableOpacity style={styles.forgotPassword} onPress={() => router.push('/(auth)/forgot-password')}>
|
||||
<Text style={styles.forgotPasswordText}>Forgot Password?</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@ -124,7 +124,7 @@ export default function LoginScreen() {
|
||||
{/* Footer */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Don't have an account? </Text>
|
||||
<TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => router.push('/(auth)/register')}>
|
||||
<Text style={styles.footerLink}>Create Account</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
219
app/(auth)/register.tsx
Normal file
219
app/(auth)/register.tsx
Normal file
@ -0,0 +1,219 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
||||
|
||||
export default function RegisterScreen() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Basic validation
|
||||
if (!username.trim()) {
|
||||
Alert.alert('Error', 'Username is required');
|
||||
return;
|
||||
}
|
||||
if (!email.trim()) {
|
||||
Alert.alert('Error', 'Email is required');
|
||||
return;
|
||||
}
|
||||
if (!password.trim()) {
|
||||
Alert.alert('Error', 'Password is required');
|
||||
return;
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
Alert.alert('Error', 'Passwords do not match');
|
||||
return;
|
||||
}
|
||||
if (!name.trim()) {
|
||||
Alert.alert('Error', 'Full name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show "in development" message
|
||||
Alert.alert(
|
||||
'Coming Soon',
|
||||
'Account registration is currently under development. Please contact support to create an account.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Back Button */}
|
||||
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Create Account</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Join WellNuo to start monitoring your loved ones
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Form */}
|
||||
<View style={styles.form}>
|
||||
<Input
|
||||
label="Username"
|
||||
placeholder="Choose a username"
|
||||
leftIcon="person-outline"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Email Address"
|
||||
placeholder="Enter your email"
|
||||
leftIcon="mail-outline"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Full Name"
|
||||
placeholder="Enter your full name"
|
||||
leftIcon="person-circle-outline"
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
autoCapitalize="words"
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Phone (optional)"
|
||||
placeholder="Enter your phone number"
|
||||
leftIcon="call-outline"
|
||||
value={phone}
|
||||
onChangeText={setPhone}
|
||||
keyboardType="phone-pad"
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Password"
|
||||
placeholder="Create a password"
|
||||
leftIcon="lock-closed-outline"
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Confirm Password"
|
||||
placeholder="Confirm your password"
|
||||
leftIcon="lock-closed-outline"
|
||||
secureTextEntry
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title="Create Account"
|
||||
onPress={handleSubmit}
|
||||
loading={isLoading}
|
||||
fullWidth
|
||||
size="lg"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Already have an account? </Text>
|
||||
<TouchableOpacity onPress={() => router.back()}>
|
||||
<Text style={styles.footerLink}>Sign In</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: AppColors.background,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: Spacing.lg,
|
||||
paddingTop: Spacing.xl,
|
||||
paddingBottom: Spacing.xl,
|
||||
},
|
||||
backButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: BorderRadius.full,
|
||||
backgroundColor: AppColors.surface,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: Spacing.lg,
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginBottom: Spacing.xl,
|
||||
},
|
||||
title: {
|
||||
fontSize: FontSizes['2xl'],
|
||||
fontWeight: '700',
|
||||
color: AppColors.textPrimary,
|
||||
marginBottom: Spacing.sm,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.textSecondary,
|
||||
textAlign: 'center',
|
||||
},
|
||||
form: {
|
||||
marginBottom: Spacing.xl,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.textSecondary,
|
||||
},
|
||||
footerLink: {
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
@ -6,6 +6,12 @@ import {
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
RefreshControl,
|
||||
Image,
|
||||
Modal,
|
||||
TextInput,
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useLocalSearchParams, router } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
@ -26,6 +32,13 @@ export default function BeneficiaryDetailScreen() {
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Edit modal state
|
||||
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
||||
const [editForm, setEditForm] = useState({
|
||||
name: '',
|
||||
address: '',
|
||||
});
|
||||
|
||||
const loadBeneficiary = useCallback(async (showLoading = true) => {
|
||||
if (!id) return;
|
||||
|
||||
@ -66,6 +79,32 @@ export default function BeneficiaryDetailScreen() {
|
||||
router.push('/(tabs)/chat');
|
||||
};
|
||||
|
||||
const handleEditPress = () => {
|
||||
if (beneficiary) {
|
||||
setEditForm({
|
||||
name: beneficiary.name || '',
|
||||
address: beneficiary.address || '',
|
||||
});
|
||||
setIsEditModalVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEdit = () => {
|
||||
Alert.alert(
|
||||
'Demo Mode',
|
||||
'This is a mockup feature. Saving beneficiary data requires backend API endpoints that are not yet implemented.\n\nContact your administrator to enable this feature.',
|
||||
[{ text: 'OK', onPress: () => setIsEditModalVisible(false) }]
|
||||
);
|
||||
};
|
||||
|
||||
const showComingSoon = (featureName: string) => {
|
||||
Alert.alert(
|
||||
'Coming Soon',
|
||||
`${featureName} is currently in development.\n\nThis feature will be available in a future update.`,
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner fullScreen message="Loading beneficiary data..." />;
|
||||
}
|
||||
@ -90,8 +129,8 @@ export default function BeneficiaryDetailScreen() {
|
||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>{beneficiary.name}</Text>
|
||||
<TouchableOpacity style={styles.menuButton}>
|
||||
<Ionicons name="ellipsis-vertical" size={24} color={AppColors.textPrimary} />
|
||||
<TouchableOpacity style={styles.editButton} onPress={handleEditPress}>
|
||||
<Ionicons name="create-outline" size={24} color={AppColors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@ -109,9 +148,13 @@ export default function BeneficiaryDetailScreen() {
|
||||
{/* Beneficiary Info Card */}
|
||||
<View style={styles.infoCard}>
|
||||
<View style={styles.avatarContainer}>
|
||||
<Text style={styles.avatarText}>
|
||||
{beneficiary.name.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
{beneficiary.avatar ? (
|
||||
<Image source={{ uri: beneficiary.avatar }} style={styles.avatarImage} />
|
||||
) : (
|
||||
<Text style={styles.avatarText}>
|
||||
{beneficiary.name.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
styles.statusBadge,
|
||||
@ -231,21 +274,21 @@ export default function BeneficiaryDetailScreen() {
|
||||
<Text style={styles.actionLabel}>Chat with Julia</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.actionCard}>
|
||||
<TouchableOpacity style={styles.actionCard} onPress={() => showComingSoon('Set Reminder')}>
|
||||
<View style={[styles.actionIcon, { backgroundColor: '#FEF3C7' }]}>
|
||||
<Ionicons name="notifications" size={24} color={AppColors.warning} />
|
||||
</View>
|
||||
<Text style={styles.actionLabel}>Set Reminder</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.actionCard}>
|
||||
<TouchableOpacity style={styles.actionCard} onPress={() => showComingSoon('Video Call')}>
|
||||
<View style={[styles.actionIcon, { backgroundColor: '#DBEAFE' }]}>
|
||||
<Ionicons name="call" size={24} color={AppColors.primary} />
|
||||
</View>
|
||||
<Text style={styles.actionLabel}>Video Call</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.actionCard}>
|
||||
<TouchableOpacity style={styles.actionCard} onPress={() => showComingSoon('Activity Report')}>
|
||||
<View style={[styles.actionIcon, { backgroundColor: '#F3E8FF' }]}>
|
||||
<Ionicons name="analytics" size={24} color="#9333EA" />
|
||||
</View>
|
||||
@ -264,6 +307,76 @@ export default function BeneficiaryDetailScreen() {
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Edit Modal */}
|
||||
<Modal
|
||||
visible={isEditModalVisible}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={() => setIsEditModalVisible(false)}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.modalOverlay}
|
||||
>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>Edit Beneficiary</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => setIsEditModalVisible(false)}
|
||||
style={styles.modalCloseButton}
|
||||
>
|
||||
<Ionicons name="close" size={24} color={AppColors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.modalForm}>
|
||||
<Text style={styles.inputLabel}>Name</Text>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
value={editForm.name}
|
||||
onChangeText={(text) => setEditForm({ ...editForm, name: text })}
|
||||
placeholder="Enter name"
|
||||
placeholderTextColor={AppColors.textMuted}
|
||||
/>
|
||||
|
||||
<Text style={styles.inputLabel}>Address</Text>
|
||||
<TextInput
|
||||
style={[styles.textInput, styles.textAreaInput]}
|
||||
value={editForm.address}
|
||||
onChangeText={(text) => setEditForm({ ...editForm, address: text })}
|
||||
placeholder="Enter address"
|
||||
placeholderTextColor={AppColors.textMuted}
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
/>
|
||||
|
||||
<View style={styles.infoBox}>
|
||||
<Ionicons name="information-circle-outline" size={20} color={AppColors.primary} />
|
||||
<Text style={styles.infoBoxText}>
|
||||
Other data (wellness score, temperature, sleep hours) is collected automatically from sensors.
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.modalFooter}>
|
||||
<TouchableOpacity
|
||||
style={styles.cancelButton}
|
||||
onPress={() => setIsEditModalVisible(false)}
|
||||
>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.saveButton}
|
||||
onPress={handleSaveEdit}
|
||||
>
|
||||
<Ionicons name="checkmark" size={20} color={AppColors.white} />
|
||||
<Text style={styles.saveButtonText}>Save Changes</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@ -291,7 +404,7 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '600',
|
||||
color: AppColors.textPrimary,
|
||||
},
|
||||
menuButton: {
|
||||
editButton: {
|
||||
padding: Spacing.xs,
|
||||
},
|
||||
content: {
|
||||
@ -317,6 +430,11 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '600',
|
||||
color: AppColors.white,
|
||||
},
|
||||
avatarImage: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: BorderRadius.full,
|
||||
},
|
||||
statusBadge: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
@ -423,4 +541,104 @@ const styles = StyleSheet.create({
|
||||
padding: Spacing.lg,
|
||||
paddingBottom: Spacing.xxl,
|
||||
},
|
||||
// Modal styles
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: AppColors.background,
|
||||
borderTopLeftRadius: BorderRadius.xl,
|
||||
borderTopRightRadius: BorderRadius.xl,
|
||||
maxHeight: '80%',
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: Spacing.lg,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: AppColors.border,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: FontSizes.xl,
|
||||
fontWeight: '600',
|
||||
color: AppColors.textPrimary,
|
||||
},
|
||||
modalCloseButton: {
|
||||
padding: Spacing.xs,
|
||||
},
|
||||
modalForm: {
|
||||
padding: Spacing.lg,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: FontSizes.sm,
|
||||
fontWeight: '500',
|
||||
color: AppColors.textSecondary,
|
||||
marginBottom: Spacing.xs,
|
||||
marginTop: Spacing.md,
|
||||
},
|
||||
textInput: {
|
||||
backgroundColor: AppColors.surface,
|
||||
borderRadius: BorderRadius.md,
|
||||
padding: Spacing.md,
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.textPrimary,
|
||||
borderWidth: 1,
|
||||
borderColor: AppColors.border,
|
||||
},
|
||||
textAreaInput: {
|
||||
minHeight: 80,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
infoBox: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#EFF6FF',
|
||||
borderRadius: BorderRadius.md,
|
||||
padding: Spacing.md,
|
||||
marginTop: Spacing.lg,
|
||||
gap: Spacing.sm,
|
||||
},
|
||||
infoBoxText: {
|
||||
flex: 1,
|
||||
fontSize: FontSizes.sm,
|
||||
color: AppColors.primary,
|
||||
lineHeight: 20,
|
||||
},
|
||||
modalFooter: {
|
||||
flexDirection: 'row',
|
||||
padding: Spacing.lg,
|
||||
gap: Spacing.md,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: AppColors.border,
|
||||
},
|
||||
cancelButton: {
|
||||
flex: 1,
|
||||
paddingVertical: Spacing.md,
|
||||
borderRadius: BorderRadius.md,
|
||||
backgroundColor: AppColors.surface,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
cancelButtonText: {
|
||||
fontSize: FontSizes.base,
|
||||
fontWeight: '500',
|
||||
color: AppColors.textSecondary,
|
||||
},
|
||||
saveButton: {
|
||||
flex: 2,
|
||||
flexDirection: 'row',
|
||||
paddingVertical: Spacing.md,
|
||||
borderRadius: BorderRadius.md,
|
||||
backgroundColor: AppColors.primary,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: Spacing.xs,
|
||||
},
|
||||
saveButtonText: {
|
||||
fontSize: FontSizes.base,
|
||||
fontWeight: '600',
|
||||
color: AppColors.white,
|
||||
},
|
||||
});
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
TouchableOpacity,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
@ -142,7 +143,10 @@ export default function ChatScreen() {
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.headerButton}>
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
onPress={() => Alert.alert('Coming Soon', 'Chat settings will be available in a future update.')}
|
||||
>
|
||||
<Ionicons name="ellipsis-vertical" size={24} color={AppColors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@ -6,7 +6,8 @@ import {
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
RefreshControl
|
||||
RefreshControl,
|
||||
Image,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
@ -24,7 +25,6 @@ interface PatientCardProps {
|
||||
}
|
||||
|
||||
function PatientCard({ patient, onPress }: PatientCardProps) {
|
||||
const isOnline = patient.status === 'online';
|
||||
const wellnessColor = patient.wellness_score && patient.wellness_score >= 70
|
||||
? AppColors.success
|
||||
: patient.wellness_score && patient.wellness_score >= 40
|
||||
@ -35,11 +35,16 @@ function PatientCard({ patient, onPress }: PatientCardProps) {
|
||||
<TouchableOpacity style={styles.card} onPress={onPress} activeOpacity={0.7}>
|
||||
<View style={styles.cardContent}>
|
||||
{/* Avatar */}
|
||||
<View style={[styles.avatar, isOnline && styles.avatarOnline]}>
|
||||
<Text style={styles.avatarText}>
|
||||
{patient.name.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
{isOnline && <View style={styles.onlineIndicator} />}
|
||||
<View style={styles.avatarWrapper}>
|
||||
{patient.avatar ? (
|
||||
<Image source={{ uri: patient.avatar }} style={styles.avatarImage} />
|
||||
) : (
|
||||
<View style={styles.avatar}>
|
||||
<Text style={styles.avatarText}>
|
||||
{patient.name.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Info */}
|
||||
@ -51,16 +56,9 @@ function PatientCard({ patient, onPress }: PatientCardProps) {
|
||||
<Text style={styles.locationText}>{patient.last_location}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.statusRow}>
|
||||
<View style={[styles.statusBadge, isOnline ? styles.statusOnline : styles.statusOffline]}>
|
||||
<Text style={[styles.statusText, isOnline ? styles.statusTextOnline : styles.statusTextOffline]}>
|
||||
{isOnline ? 'Active' : 'Inactive'}
|
||||
</Text>
|
||||
</View>
|
||||
{patient.last_activity && (
|
||||
<Text style={styles.lastActivity}>{patient.last_activity}</Text>
|
||||
)}
|
||||
</View>
|
||||
{patient.last_activity && (
|
||||
<Text style={styles.lastActivity}>{patient.last_activity}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Wellness Score */}
|
||||
@ -266,35 +264,31 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
padding: Spacing.md,
|
||||
},
|
||||
avatarWrapper: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
avatar: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: BorderRadius.full,
|
||||
borderRadius: 28,
|
||||
backgroundColor: AppColors.primaryLight,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
},
|
||||
avatarOnline: {
|
||||
borderWidth: 2,
|
||||
borderColor: AppColors.success,
|
||||
avatarImage: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
},
|
||||
avatarText: {
|
||||
fontSize: FontSizes.xl,
|
||||
fontWeight: '600',
|
||||
color: AppColors.white,
|
||||
},
|
||||
onlineIndicator: {
|
||||
position: 'absolute',
|
||||
bottom: 2,
|
||||
right: 2,
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 7,
|
||||
backgroundColor: AppColors.success,
|
||||
borderWidth: 2,
|
||||
borderColor: AppColors.white,
|
||||
},
|
||||
info: {
|
||||
flex: 1,
|
||||
marginLeft: Spacing.md,
|
||||
@ -309,32 +303,6 @@ const styles = StyleSheet.create({
|
||||
color: AppColors.textSecondary,
|
||||
marginTop: 2,
|
||||
},
|
||||
statusRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: Spacing.xs,
|
||||
},
|
||||
statusBadge: {
|
||||
paddingHorizontal: Spacing.sm,
|
||||
paddingVertical: 2,
|
||||
borderRadius: BorderRadius.sm,
|
||||
},
|
||||
statusOnline: {
|
||||
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
||||
},
|
||||
statusOffline: {
|
||||
backgroundColor: 'rgba(107, 114, 128, 0.1)',
|
||||
},
|
||||
statusText: {
|
||||
fontSize: FontSizes.xs,
|
||||
fontWeight: '500',
|
||||
},
|
||||
statusTextOnline: {
|
||||
color: AppColors.success,
|
||||
},
|
||||
statusTextOffline: {
|
||||
color: AppColors.textMuted,
|
||||
},
|
||||
lastActivity: {
|
||||
fontSize: FontSizes.xs,
|
||||
color: AppColors.textMuted,
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
Linking,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
@ -209,7 +210,11 @@ export default function HelpScreen() {
|
||||
{ title: 'Understanding Data', duration: '4:15' },
|
||||
{ title: 'Troubleshooting', duration: '5:00' },
|
||||
].map((video, index) => (
|
||||
<TouchableOpacity key={index} style={styles.videoCard}>
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={styles.videoCard}
|
||||
onPress={() => Alert.alert('Coming Soon', `"${video.title}" video tutorial will be available in a future update.`)}
|
||||
>
|
||||
<View style={styles.videoThumbnail}>
|
||||
<Ionicons name="play-circle" size={40} color={AppColors.white} />
|
||||
</View>
|
||||
|
||||
@ -374,7 +374,10 @@ export default function SupportScreen() {
|
||||
numberOfLines={4}
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
<TouchableOpacity style={styles.replyButton}>
|
||||
<TouchableOpacity
|
||||
style={styles.replyButton}
|
||||
onPress={() => Alert.alert('Coming Soon', 'Reply functionality will be available in a future update.')}
|
||||
>
|
||||
<Text style={styles.replyButtonText}>Send Reply</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@ -4,14 +4,13 @@ import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError, Da
|
||||
const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api';
|
||||
const CLIENT_ID = 'MA_001';
|
||||
|
||||
// Avatar images for elderly beneficiaries (stable Unsplash URLs)
|
||||
// Avatar images for elderly beneficiaries - grandmothers (бабушки)
|
||||
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
|
||||
'https://images.unsplash.com/photo-1566616213894-2d4e1baee5d8?w=200&h=200&fit=crop&crop=face', // grandmother with gray hair
|
||||
'https://images.unsplash.com/photo-1544027993-37dbfe43562a?w=200&h=200&fit=crop&crop=face', // elderly woman smiling
|
||||
'https://images.unsplash.com/photo-1491308056676-205b7c9a7dc1?w=200&h=200&fit=crop&crop=face', // senior woman portrait
|
||||
'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=200&h=200&fit=crop&crop=face', // older woman glasses
|
||||
'https://images.unsplash.com/photo-1548142813-c348350df52b?w=200&h=200&fit=crop&crop=face', // grandmother portrait
|
||||
];
|
||||
|
||||
// Get consistent avatar based on deployment_id
|
||||
@ -283,9 +282,11 @@ class ApiService {
|
||||
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);
|
||||
patients.push({
|
||||
id: parseInt(data.deployment_id, 10),
|
||||
id: deploymentId,
|
||||
name: data.name,
|
||||
avatar: getAvatarForBeneficiary(deploymentId),
|
||||
status: isRecent ? 'online' : 'offline',
|
||||
address: data.address,
|
||||
timezone: data.time_zone,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user