diff --git a/app/(tabs)/beneficiaries/[id]/index.tsx b/app/(tabs)/beneficiaries/[id]/index.tsx index 6242fec..402a482 100644 --- a/app/(tabs)/beneficiaries/[id]/index.tsx +++ b/app/(tabs)/beneficiaries/[id]/index.tsx @@ -125,45 +125,47 @@ export default function BeneficiaryDetailScreen() { {beneficiary.name} - {beneficiary.relationship} + {beneficiary.address && ( + {beneficiary.address} + )} - Last activity: {beneficiary.last_activity} + {beneficiary.last_location ? `📍 ${beneficiary.last_location}` : ''} • {beneficiary.last_activity || 'No recent activity'} - {/* Sensor Stats */} + {/* Sensor Stats - using real API data */} Sensor Overview - + - {beneficiary.sensor_data?.motion_detected ? 'Active' : 'Inactive'} + {beneficiary.status === 'online' ? 'Active' : 'Inactive'} - Motion - {beneficiary.sensor_data?.last_motion || '--'} + Status + {beneficiary.last_activity || '--'} - + - - {beneficiary.sensor_data?.door_status === 'open' ? 'Open' : 'Closed'} + + {beneficiary.last_location || '--'} - Door Status - Main entrance + Location + {beneficiary.before_last_location || '--'} @@ -171,10 +173,48 @@ export default function BeneficiaryDetailScreen() { - {beneficiary.sensor_data?.temperature || '--'}°C + {beneficiary.temperature?.toFixed(1) || '--'}{beneficiary.units || '°F'} Temperature - {beneficiary.sensor_data?.humidity || '--'}% humidity + Bedroom: {beneficiary.bedroom_temperature?.toFixed(1) || '--'}{beneficiary.units || '°F'} + + + + {/* Additional stats row */} + + + + + + + {beneficiary.sleep_hours?.toFixed(1) || '--'}h + + Sleep + + + + = 70 ? '#D1FAE5' : '#FEF3C7' }]}> + = 70 ? AppColors.success : AppColors.warning} + /> + + + {beneficiary.wellness_score || '--'}% + + Wellness + {beneficiary.wellness_descriptor || '--'} + + + + + + + + {beneficiary.address?.split(',')[0] || '--'} + + Address diff --git a/app/(tabs)/profile/_layout.tsx b/app/(tabs)/profile/_layout.tsx new file mode 100644 index 0000000..ed7390d --- /dev/null +++ b/app/(tabs)/profile/_layout.tsx @@ -0,0 +1,6 @@ +import { Slot } from 'expo-router'; + +export default function ProfileLayout() { + // Using Slot instead of Stack to keep tab bar visible + return ; +} diff --git a/app/profile/about.tsx b/app/(tabs)/profile/about.tsx similarity index 80% rename from app/profile/about.tsx rename to app/(tabs)/profile/about.tsx index d68f6f2..321a2fa 100644 --- a/app/profile/about.tsx +++ b/app/(tabs)/profile/about.tsx @@ -11,6 +11,7 @@ import { import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; +import { PageHeader } from '@/components/PageHeader'; interface InfoRowProps { label: string; @@ -48,14 +49,16 @@ export default function AboutScreen() { }; return ( - + + {/* App Logo & Name */} - - - - WellNuo + Caring for Those Who Matter Most @@ -156,32 +159,6 @@ export default function AboutScreen() { title="Follow on Twitter" onPress={() => openURL('https://twitter.com/wellnuo')} /> - - openURL('https://github.com/wellnuo')} - /> - - - - {/* Acknowledgments */} - - Acknowledgments - - - WellNuo uses the following open-source software: - - • React Native (MIT License) - • Expo (MIT License) - • React Navigation (MIT License) - • And many other wonderful packages - openURL('https://wellnuo.com/licenses')} - > - View All Licenses - @@ -208,19 +185,10 @@ const styles = StyleSheet.create({ paddingVertical: Spacing.xl, backgroundColor: AppColors.background, }, - logoContainer: { - width: 100, + logoImage: { + width: 180, height: 100, - borderRadius: 24, - backgroundColor: AppColors.primary, - justifyContent: 'center', - alignItems: 'center', - marginBottom: Spacing.md, - }, - appName: { - fontSize: 32, - fontWeight: '700', - color: AppColors.textPrimary, + marginBottom: Spacing.sm, }, appTagline: { fontSize: FontSizes.base, @@ -313,28 +281,6 @@ const styles = StyleSheet.create({ backgroundColor: AppColors.border, 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: { alignItems: 'center', paddingVertical: Spacing.xl, diff --git a/app/profile/edit.tsx b/app/(tabs)/profile/edit.tsx similarity index 98% rename from app/profile/edit.tsx rename to app/(tabs)/profile/edit.tsx index c506506..70d3597 100644 --- a/app/profile/edit.tsx +++ b/app/(tabs)/profile/edit.tsx @@ -14,6 +14,7 @@ import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import { router } from 'expo-router'; import { useAuth } from '@/contexts/AuthContext'; +import { PageHeader } from '@/components/PageHeader'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; export default function EditProfileScreen() { @@ -58,7 +59,8 @@ export default function EditProfileScreen() { }; return ( - + + + + {/* Search */} diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile/index.tsx similarity index 90% rename from app/(tabs)/profile.tsx rename to app/(tabs)/profile/index.tsx index 2f9b61b..18dfac4 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile/index.tsx @@ -57,7 +57,6 @@ export default function ProfileScreen() { // Settings states const [pushNotifications, setPushNotifications] = useState(true); const [emailNotifications, setEmailNotifications] = useState(false); - const [darkMode, setDarkMode] = useState(false); const [biometricLogin, setBiometricLogin] = useState(false); const handleLogout = () => { @@ -80,17 +79,17 @@ export default function ProfileScreen() { }; // Navigation handlers - now using actual page navigation - const handleEditProfile = () => router.push('/profile/edit'); - const handleNotifications = () => router.push('/profile/notifications'); - const handlePrivacy = () => router.push('/profile/privacy'); - const handleUpgrade = () => router.push('/profile/subscription'); - const handlePayment = () => router.push('/profile/subscription'); - const handleHelp = () => router.push('/profile/help'); - const handleSupport = () => router.push('/profile/support'); - const handleTerms = () => router.push('/profile/terms'); - const handlePrivacyPolicy = () => router.push('/profile/privacy-policy'); - const handleLanguage = () => router.push('/profile/language'); - const handleAbout = () => router.push('/profile/about'); + const handleEditProfile = () => router.push('/(tabs)/profile/edit'); + const handleNotifications = () => router.push('/(tabs)/profile/notifications'); + const handlePrivacy = () => router.push('/(tabs)/profile/privacy'); + const handleUpgrade = () => router.push('/(tabs)/profile/subscription'); + const handlePayment = () => router.push('/(tabs)/profile/subscription'); + const handleHelp = () => router.push('/(tabs)/profile/help'); + const handleSupport = () => router.push('/(tabs)/profile/support'); + const handleTerms = () => router.push('/(tabs)/profile/terms'); + const handlePrivacyPolicy = () => router.push('/(tabs)/profile/privacy-policy'); + const handleLanguage = () => router.push('/(tabs)/profile/language'); + const handleAbout = () => router.push('/(tabs)/profile/about'); const handleDevInfo = () => { Alert.alert( @@ -221,27 +220,6 @@ export default function ProfileScreen() { } /> - { - 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'} - /> - } - /> - !l.available); return ( - + + {/* Current Language */} diff --git a/app/profile/notifications.tsx b/app/(tabs)/profile/notifications.tsx similarity index 98% rename from app/profile/notifications.tsx rename to app/(tabs)/profile/notifications.tsx index 2530a59..4c72b65 100644 --- a/app/profile/notifications.tsx +++ b/app/(tabs)/profile/notifications.tsx @@ -12,6 +12,7 @@ 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'; +import { PageHeader } from '@/components/PageHeader'; interface NotificationSettingProps { icon: keyof typeof Ionicons.glyphMap; @@ -89,7 +90,8 @@ export default function NotificationsScreen() { }; return ( - + + {/* Alert Types */} diff --git a/app/profile/privacy-policy.tsx b/app/(tabs)/profile/privacy-policy.tsx similarity index 98% rename from app/profile/privacy-policy.tsx rename to app/(tabs)/profile/privacy-policy.tsx index e221d0c..9ea08e7 100644 --- a/app/profile/privacy-policy.tsx +++ b/app/(tabs)/profile/privacy-policy.tsx @@ -8,6 +8,7 @@ import { import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; +import { PageHeader } from '@/components/PageHeader'; interface PrivacyHighlightProps { icon: keyof typeof Ionicons.glyphMap; @@ -39,7 +40,8 @@ function PrivacyHighlight({ export default function PrivacyPolicyScreen() { return ( - + + {/* Highlights */} diff --git a/app/profile/privacy.tsx b/app/(tabs)/profile/privacy.tsx similarity index 99% rename from app/profile/privacy.tsx rename to app/(tabs)/profile/privacy.tsx index 1af9127..505cf39 100644 --- a/app/profile/privacy.tsx +++ b/app/(tabs)/profile/privacy.tsx @@ -15,6 +15,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { router } from 'expo-router'; import { useAuth } from '@/contexts/AuthContext'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; +import { PageHeader } from '@/components/PageHeader'; interface SecurityItemProps { icon: keyof typeof Ionicons.glyphMap; @@ -193,7 +194,8 @@ export default function PrivacyScreen() { }; return ( - + + {/* Password & Authentication */} diff --git a/app/profile/subscription.tsx b/app/(tabs)/profile/subscription.tsx similarity index 98% rename from app/profile/subscription.tsx rename to app/(tabs)/profile/subscription.tsx index e6712e3..64f7851 100644 --- a/app/profile/subscription.tsx +++ b/app/(tabs)/profile/subscription.tsx @@ -11,6 +11,7 @@ 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'; +import { PageHeader } from '@/components/PageHeader'; interface PlanFeatureProps { text: string; @@ -64,7 +65,8 @@ export default function SubscriptionScreen() { }; return ( - + + {/* Current Plan Badge */} diff --git a/app/(tabs)/profile/support.tsx b/app/(tabs)/profile/support.tsx new file mode 100644 index 0000000..6d97841 --- /dev/null +++ b/app/(tabs)/profile/support.tsx @@ -0,0 +1,1068 @@ +import React, { useState } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + TextInput, + Linking, + Alert, + KeyboardAvoidingView, + Platform, + Modal, +} 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'; +import { PageHeader } from '@/components/PageHeader'; + +// Mock data for tickets +const mockTickets = [ + { + id: 'WN-2024-12341', + subject: 'Notifications not working', + category: 'Technical Issue', + status: 'resolved', + createdAt: '2024-12-08', + updatedAt: '2024-12-10', + messages: [ + { + from: 'user', + text: 'I am not receiving push notifications when my mother triggers an alert. I have notifications enabled in the app settings.', + date: '2024-12-08 14:32', + }, + { + from: 'support', + text: 'Thank you for reaching out! Let me help you with this. Could you please confirm that notifications are also enabled in your device settings (Settings > WellNuo > Notifications)?', + date: '2024-12-08 15:45', + agent: 'Sarah M.', + }, + { + from: 'user', + text: 'Oh, I checked and they were disabled there. I enabled them now.', + date: '2024-12-09 09:12', + }, + { + from: 'support', + text: 'Great! That should fix it. Please let us know if you have any other issues. Have a wonderful day!', + date: '2024-12-09 09:30', + agent: 'Sarah M.', + }, + ], + }, + { + id: 'WN-2024-12338', + subject: 'How to add a second beneficiary?', + category: 'Account Help', + status: 'resolved', + createdAt: '2024-12-05', + updatedAt: '2024-12-06', + messages: [ + { + from: 'user', + text: 'I want to add my father as a second beneficiary. How can I do that?', + date: '2024-12-05 11:20', + }, + { + from: 'support', + text: 'Hello! To add a second beneficiary:\n\n1. Go to Dashboard\n2. Tap the + button or "Add Beneficiary"\n3. Enter their details and sensor ID\n\nNote: Free plan supports 1 beneficiary. For multiple beneficiaries, consider upgrading to WellNuo Pro. Would you like more information about our plans?', + date: '2024-12-05 12:15', + agent: 'Mike R.', + }, + { + from: 'user', + text: 'Thank you! I upgraded to Pro and it works now.', + date: '2024-12-06 08:45', + }, + ], + }, + { + id: 'WN-2024-12345', + subject: 'Battery status shows wrong percentage', + category: 'Technical Issue', + status: 'open', + createdAt: '2024-12-11', + updatedAt: '2024-12-11', + messages: [ + { + from: 'user', + text: 'The sensor shows 15% battery in the app, but I just charged it fully yesterday. Is there something wrong with the sensor?', + date: '2024-12-11 16:20', + }, + { + from: 'support', + text: 'Hi there! This could be a sync issue. Please try the following:\n\n1. Force close the WellNuo app\n2. Wait 30 seconds\n3. Reopen the app\n\nIf the issue persists, try restarting the sensor by pressing the reset button for 5 seconds. Let me know the results!', + date: '2024-12-11 17:05', + agent: 'Alex K.', + }, + ], + }, + { + id: 'WN-2024-12340', + subject: 'Request: Add medication reminders', + category: 'Feature Request', + status: 'in_progress', + createdAt: '2024-12-07', + updatedAt: '2024-12-10', + messages: [ + { + from: 'user', + text: 'It would be great if WellNuo could also send medication reminders to my beneficiaries. Is this something you\'re considering?', + date: '2024-12-07 10:15', + }, + { + from: 'support', + text: 'Thank you for your feedback! We love hearing feature suggestions from our users.\n\nMedication reminders are actually on our roadmap! Our development team is working on this feature, and we expect to release it in Q1 2025.\n\nWe\'ve added your vote to this feature request. You\'ll be notified when it becomes available!', + date: '2024-12-07 14:30', + agent: 'Emma T.', + }, + { + from: 'support', + text: 'Quick update: The medication reminders feature has moved to active development. We\'re targeting a late January release. Stay tuned!', + date: '2024-12-10 09:00', + agent: 'Emma T.', + }, + ], + }, +]; + +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 ( + + + + + + {title} + {subtitle} + + + + ); +} + +const getStatusColor = (status: string) => { + switch (status) { + case 'open': + return '#3B82F6'; + case 'in_progress': + return '#F59E0B'; + case 'resolved': + return '#10B981'; + default: + return AppColors.textMuted; + } +}; + +const getStatusBgColor = (status: string) => { + switch (status) { + case 'open': + return '#DBEAFE'; + case 'in_progress': + return '#FEF3C7'; + case 'resolved': + return '#D1FAE5'; + default: + return AppColors.surface; + } +}; + +const getStatusLabel = (status: string) => { + switch (status) { + case 'open': + return 'Open'; + case 'in_progress': + return 'In Progress'; + case 'resolved': + return 'Resolved'; + default: + return status; + } +}; + +export default function SupportScreen() { + const [subject, setSubject] = useState(''); + const [message, setMessage] = useState(''); + const [category, setCategory] = useState(''); + const [isSending, setIsSending] = useState(false); + const [selectedTicket, setSelectedTicket] = useState(null); + const [showTicketModal, setShowTicketModal] = useState(false); + const [activeTab, setActiveTab] = useState<'contact' | 'tickets'>('tickets'); + + 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); + setSubject(''); + setMessage(''); + setCategory(''); + Alert.alert( + 'Ticket Submitted', + 'Thank you for contacting us!\n\nTicket #WN-2024-12346\n\nWe\'ll respond within 24 hours.', + [{ text: 'OK' }] + ); + }; + + const openTicket = (ticket: typeof mockTickets[0]) => { + setSelectedTicket(ticket); + setShowTicketModal(true); + }; + + const renderTicketModal = () => ( + setShowTicketModal(false)} + > + + {/* Modal Header */} + + setShowTicketModal(false)}> + + + Ticket Details + + + + {selectedTicket && ( + + {/* Ticket Info */} + + + Ticket ID + {selectedTicket.id} + + + Category + {selectedTicket.category} + + + Status + + + {getStatusLabel(selectedTicket.status)} + + + + + Created + {selectedTicket.createdAt} + + + + {/* Subject */} + + Subject + {selectedTicket.subject} + + + {/* Messages */} + + Conversation + {selectedTicket.messages.map((msg, index) => ( + + + + + + {msg.from === 'user' ? 'You' : `Support (${(msg as any).agent})`} + + + {msg.date} + + {msg.text} + + ))} + + + {/* Reply section for open tickets */} + {selectedTicket.status !== 'resolved' && ( + + Reply + + + Send Reply + + + )} + + )} + + + ); + + return ( + + + + {/* Tabs */} + + setActiveTab('tickets')} + > + + + My Tickets + + + setActiveTab('contact')} + > + + + New Ticket + + + + + + {activeTab === 'tickets' ? ( + <> + {/* My Tickets */} + + My Tickets ({mockTickets.length}) + + {mockTickets.map((ticket) => ( + openTicket(ticket)} + > + + {ticket.id} + + + {getStatusLabel(ticket.status)} + + + + + {ticket.subject} + + + + + {ticket.category} + + + Updated: {ticket.updatedAt} + + + + + + {ticket.messages.length} message{ticket.messages.length !== 1 ? 's' : ''} + + + + ))} + + + + ) : ( + <> + {/* Quick Contact */} + + Quick Contact + + + + + + + + + + {/* Support Hours */} + + + + + Support Hours + Phone: Mon-Fri 8am-8pm EST + Email & Chat: 24/7 + Emergency: 24/7 + + + + + {/* Submit a Ticket */} + + Submit a Ticket + + {/* Category */} + + Category * + + {categories.map((cat) => ( + setCategory(cat)} + > + + {cat} + + + ))} + + + + {/* Subject */} + + Subject * + + + + {/* Message */} + + Message * + + + + {/* Submit Button */} + + + {isSending ? 'Sending...' : 'Submit Ticket'} + + + + + + )} + + {/* FAQ Link */} + + router.push('/(tabs)/profile/help')} + > + + + + Check our Help Center + + Find answers to common questions + + + + + + + + {/* Emergency Notice */} + + + + If you're experiencing a medical emergency, please call 911 or your local + emergency services immediately. + + + + + + {/* Ticket Detail Modal */} + {renderTicketModal()} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: AppColors.surface, + }, + keyboardView: { + flex: 1, + }, + tabsContainer: { + flexDirection: 'row', + backgroundColor: AppColors.background, + borderBottomWidth: 1, + borderBottomColor: AppColors.border, + }, + tab: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: Spacing.md, + gap: Spacing.xs, + }, + tabActive: { + borderBottomWidth: 2, + borderBottomColor: AppColors.primary, + }, + tabText: { + fontSize: FontSizes.sm, + fontWeight: '500', + color: AppColors.textMuted, + }, + tabTextActive: { + color: AppColors.primary, + }, + 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, + }, + ticketsList: { + paddingHorizontal: Spacing.lg, + gap: Spacing.sm, + }, + ticketCard: { + backgroundColor: AppColors.background, + borderRadius: BorderRadius.lg, + padding: Spacing.md, + borderWidth: 1, + borderColor: AppColors.border, + }, + ticketHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: Spacing.xs, + }, + ticketId: { + fontSize: FontSizes.xs, + fontWeight: '600', + color: AppColors.primary, + }, + statusBadge: { + paddingHorizontal: Spacing.sm, + paddingVertical: 2, + borderRadius: BorderRadius.full, + }, + statusBadgeText: { + fontSize: FontSizes.xs, + fontWeight: '600', + }, + ticketSubject: { + fontSize: FontSizes.base, + fontWeight: '500', + color: AppColors.textPrimary, + marginBottom: Spacing.sm, + }, + ticketFooter: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + ticketCategory: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + ticketCategoryText: { + fontSize: FontSizes.xs, + color: AppColors.textMuted, + }, + ticketDate: { + fontSize: FontSizes.xs, + color: AppColors.textMuted, + }, + ticketMessages: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + marginTop: Spacing.xs, + paddingTop: Spacing.xs, + borderTopWidth: 1, + borderTopColor: AppColors.border, + }, + ticketMessagesText: { + fontSize: FontSizes.xs, + color: AppColors.textMuted, + }, + 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: 100, + 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, + }, + submitButton: { + backgroundColor: AppColors.primary, + borderRadius: BorderRadius.lg, + paddingVertical: Spacing.md, + alignItems: 'center', + marginTop: Spacing.sm, + }, + 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, + }, + // Modal styles + modalContainer: { + flex: 1, + backgroundColor: AppColors.surface, + }, + modalHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: Spacing.lg, + paddingVertical: Spacing.md, + backgroundColor: AppColors.background, + borderBottomWidth: 1, + borderBottomColor: AppColors.border, + }, + modalTitle: { + fontSize: FontSizes.lg, + fontWeight: '600', + color: AppColors.textPrimary, + }, + modalContent: { + flex: 1, + }, + ticketInfo: { + backgroundColor: AppColors.background, + padding: Spacing.lg, + marginBottom: Spacing.md, + }, + ticketInfoRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: Spacing.xs, + }, + ticketInfoLabel: { + fontSize: FontSizes.sm, + color: AppColors.textSecondary, + }, + ticketInfoValue: { + fontSize: FontSizes.sm, + fontWeight: '500', + color: AppColors.textPrimary, + }, + ticketSubjectContainer: { + backgroundColor: AppColors.background, + padding: Spacing.lg, + marginBottom: Spacing.md, + }, + ticketSubjectLabel: { + fontSize: FontSizes.sm, + color: AppColors.textSecondary, + marginBottom: Spacing.xs, + }, + ticketSubjectText: { + fontSize: FontSizes.base, + fontWeight: '600', + color: AppColors.textPrimary, + }, + messagesContainer: { + padding: Spacing.lg, + }, + messagesTitle: { + fontSize: FontSizes.sm, + fontWeight: '600', + color: AppColors.textSecondary, + marginBottom: Spacing.md, + textTransform: 'uppercase', + }, + messageCard: { + backgroundColor: AppColors.background, + borderRadius: BorderRadius.lg, + padding: Spacing.md, + marginBottom: Spacing.sm, + borderLeftWidth: 3, + borderLeftColor: AppColors.primary, + }, + messageCardSupport: { + borderLeftColor: '#10B981', + backgroundColor: '#F0FDF4', + }, + messageHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: Spacing.xs, + }, + messageFrom: { + flexDirection: 'row', + alignItems: 'center', + gap: Spacing.xs, + }, + messageFromText: { + fontSize: FontSizes.sm, + fontWeight: '600', + color: AppColors.textPrimary, + }, + messageDate: { + fontSize: FontSizes.xs, + color: AppColors.textMuted, + }, + messageText: { + fontSize: FontSizes.sm, + color: AppColors.textPrimary, + lineHeight: 20, + }, + replySection: { + padding: Spacing.lg, + backgroundColor: AppColors.background, + marginTop: Spacing.md, + }, + replyTitle: { + fontSize: FontSizes.sm, + fontWeight: '600', + color: AppColors.textSecondary, + marginBottom: Spacing.sm, + textTransform: 'uppercase', + }, + replyInput: { + backgroundColor: AppColors.surface, + borderRadius: BorderRadius.md, + paddingHorizontal: Spacing.md, + paddingVertical: Spacing.md, + fontSize: FontSizes.base, + color: AppColors.textPrimary, + borderWidth: 1, + borderColor: AppColors.border, + minHeight: 100, + marginBottom: Spacing.md, + }, + replyButton: { + backgroundColor: AppColors.primary, + borderRadius: BorderRadius.lg, + paddingVertical: Spacing.md, + alignItems: 'center', + }, + replyButtonText: { + fontSize: FontSizes.base, + fontWeight: '600', + color: AppColors.white, + }, +}); diff --git a/app/profile/terms.tsx b/app/(tabs)/profile/terms.tsx similarity index 98% rename from app/profile/terms.tsx rename to app/(tabs)/profile/terms.tsx index c8fb019..b2a2022 100644 --- a/app/profile/terms.tsx +++ b/app/(tabs)/profile/terms.tsx @@ -7,10 +7,12 @@ import { } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; +import { PageHeader } from '@/components/PageHeader'; export default function TermsScreen() { return ( - + + Last Updated: December 2024 diff --git a/app/profile/_layout.tsx b/app/profile/_layout.tsx deleted file mode 100644 index 1a82841..0000000 --- a/app/profile/_layout.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Stack } from 'expo-router'; -import { AppColors } from '@/constants/theme'; - -export default function ProfileLayout() { - return ( - - - - - - - - - - - - - ); -} diff --git a/app/profile/support.tsx b/app/profile/support.tsx deleted file mode 100644 index 515c5b8..0000000 --- a/app/profile/support.tsx +++ /dev/null @@ -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 ( - - - - - - {title} - {subtitle} - - - - ); -} - -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 ( - - - - {/* Quick Contact */} - - Quick Contact - - - - - - - - - - {/* Support Hours */} - - - - - Support Hours - Phone: Mon-Fri 8am-8pm EST - Email & Chat: 24/7 - Emergency: 24/7 - - - - - {/* Submit a Ticket */} - - Submit a Ticket - - {/* Category */} - - Category * - - {categories.map((cat) => ( - setCategory(cat)} - > - - {cat} - - - ))} - - - - {/* Subject */} - - Subject * - - - - {/* Message */} - - Message * - - - - {/* Attachments hint */} - - - - Need to attach screenshots? Reply to your ticket email. - - - - {/* Submit Button */} - - - {isSending ? 'Sending...' : 'Submit Ticket'} - - - - - - {/* FAQ Link */} - - router.push('/profile/help')} - > - - - - Check our Help Center - - Find answers to common questions - - - - - - - - {/* Emergency Notice */} - - - - If you're experiencing a medical emergency, please call 911 or your local - emergency services immediately. - - - - - - ); -} - -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, - }, -}); diff --git a/components/PageHeader.tsx b/components/PageHeader.tsx new file mode 100644 index 0000000..f343d83 --- /dev/null +++ b/components/PageHeader.tsx @@ -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 ( + + + + + {title} + + {rightElement || } + + + ); +} + +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, + }, +}); diff --git a/services/api.ts b/services/api.ts index 7e29070..0d08608 100644 --- a/services/api.ts +++ b/services/api.ts @@ -4,6 +4,22 @@ 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) +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 function formatTimeAgo(date: Date): string { const now = new Date(); @@ -176,15 +192,39 @@ class ApiService { } async getBeneficiary(id: number): Promise> { - const response = await this.getBeneficiaries(); + // Use real API data via getPatientDashboard + const response = await this.getPatientDashboard(id.toString()); + 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); - if (!beneficiary) { - return { ok: false, error: { message: 'Beneficiary not found', code: 'NOT_FOUND' } }; - } + const data = response.data; + // Determine if patient is "online" based on last_detected_time + 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 }; }