- Backend: Update Legacy API credentials to robster/rob2 - Frontend: ROOM_LOCATIONS with icons and legacyCode mapping - Device Settings: Modal picker for room selection - api.ts: Bidirectional conversion (code ↔ name) - Various UI/UX improvements across screens PRD-DEPLOYMENT.md completed (Score: 9/10) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
432 lines
12 KiB
TypeScript
432 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
RefreshControl,
|
|
} from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { AppColors, Spacing, BorderRadius, FontSizes, FontWeights } from '@/constants/theme';
|
|
|
|
interface MockDashboardProps {
|
|
beneficiaryName: string;
|
|
}
|
|
|
|
// Mock data generator - creates realistic-looking data
|
|
const generateMockData = () => {
|
|
const now = new Date();
|
|
const hours = now.getHours();
|
|
|
|
// Generate activities based on time of day
|
|
const activities = [
|
|
{
|
|
id: 1,
|
|
type: 'walking',
|
|
icon: 'walk' as const,
|
|
title: 'Movement',
|
|
value: `${Math.floor(Math.random() * 2000 + 500)} steps`,
|
|
time: `${Math.floor(Math.random() * 30 + 10)} min ago`,
|
|
color: AppColors.success,
|
|
},
|
|
{
|
|
id: 2,
|
|
type: 'hydration',
|
|
icon: 'water' as const,
|
|
title: 'Hydration',
|
|
value: `${Math.floor(Math.random() * 4 + 2)} glasses`,
|
|
time: `${Math.floor(Math.random() * 60 + 20)} min ago`,
|
|
color: '#3B82F6',
|
|
},
|
|
{
|
|
id: 3,
|
|
type: 'bathroom',
|
|
icon: 'home' as const,
|
|
title: 'Bathroom',
|
|
value: `${Math.floor(Math.random() * 3 + 2)} visits`,
|
|
time: `${Math.floor(Math.random() * 120 + 30)} min ago`,
|
|
color: '#8B5CF6',
|
|
},
|
|
{
|
|
id: 4,
|
|
type: 'sleep',
|
|
icon: 'moon' as const,
|
|
title: 'Sleep',
|
|
value: hours < 10 ? `${(Math.random() * 2 + 6).toFixed(1)} hrs` : 'N/A',
|
|
time: hours < 10 ? 'Last night' : 'Tracking tonight',
|
|
color: '#6366F1',
|
|
},
|
|
{
|
|
id: 5,
|
|
type: 'meals',
|
|
icon: 'restaurant' as const,
|
|
title: 'Kitchen Activity',
|
|
value: `${Math.floor(Math.random() * 2 + 1)} visits`,
|
|
time: `${Math.floor(Math.random() * 180 + 60)} min ago`,
|
|
color: '#F59E0B',
|
|
},
|
|
];
|
|
|
|
return {
|
|
wellnessScore: Math.floor(Math.random() * 20 + 75), // 75-95
|
|
temperature: {
|
|
living: (Math.random() * 4 + 68).toFixed(1),
|
|
bedroom: (Math.random() * 4 + 66).toFixed(1),
|
|
},
|
|
lastActivity: {
|
|
location: ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][Math.floor(Math.random() * 4)],
|
|
time: `${Math.floor(Math.random() * 15 + 1)} min ago`,
|
|
},
|
|
activities,
|
|
alerts: Math.random() > 0.8 ? 1 : 0, // 20% chance of alert
|
|
};
|
|
};
|
|
|
|
export default function MockDashboard({ beneficiaryName }: MockDashboardProps) {
|
|
const [data, setData] = useState(generateMockData());
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
const onRefresh = () => {
|
|
setRefreshing(true);
|
|
setTimeout(() => {
|
|
setData(generateMockData());
|
|
setRefreshing(false);
|
|
}, 1000);
|
|
};
|
|
|
|
// Refresh data every 5 minutes
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
setData(generateMockData());
|
|
}, 5 * 60 * 1000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const getWellnessColor = (score: number) => {
|
|
if (score >= 80) return AppColors.success;
|
|
if (score >= 60) return AppColors.warning;
|
|
return AppColors.error;
|
|
};
|
|
|
|
return (
|
|
<ScrollView
|
|
style={styles.container}
|
|
contentContainerStyle={styles.content}
|
|
refreshControl={
|
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
}
|
|
>
|
|
{/* Mock Data Banner */}
|
|
<View style={styles.mockBanner}>
|
|
<Ionicons name="flask" size={16} color={AppColors.warning} />
|
|
<Text style={styles.mockBannerText}>
|
|
Demo Data - Connect sensors for real activity
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Wellness Score Card */}
|
|
<View style={styles.card}>
|
|
<View style={styles.cardHeader}>
|
|
<Text style={styles.cardTitle}>Wellness Score</Text>
|
|
<View style={[styles.badge, { backgroundColor: getWellnessColor(data.wellnessScore) + '20' }]}>
|
|
<Text style={[styles.badgeText, { color: getWellnessColor(data.wellnessScore) }]}>
|
|
{data.wellnessScore >= 80 ? 'Good' : data.wellnessScore >= 60 ? 'Fair' : 'Low'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.scoreContainer}>
|
|
<Text style={[styles.scoreValue, { color: getWellnessColor(data.wellnessScore) }]}>
|
|
{data.wellnessScore}%
|
|
</Text>
|
|
<View style={styles.scoreBar}>
|
|
<View
|
|
style={[
|
|
styles.scoreProgress,
|
|
{
|
|
width: `${data.wellnessScore}%`,
|
|
backgroundColor: getWellnessColor(data.wellnessScore)
|
|
}
|
|
]}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Current Status */}
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>Current Status</Text>
|
|
<View style={styles.statusRow}>
|
|
<View style={styles.statusItem}>
|
|
<Ionicons name="location" size={24} color={AppColors.primary} />
|
|
<View style={styles.statusTextContainer}>
|
|
<Text style={styles.statusLabel}>Location</Text>
|
|
<Text style={styles.statusValue}>{data.lastActivity.location}</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.statusItem}>
|
|
<Ionicons name="time" size={24} color={AppColors.primary} />
|
|
<View style={styles.statusTextContainer}>
|
|
<Text style={styles.statusLabel}>Last Activity</Text>
|
|
<Text style={styles.statusValue}>{data.lastActivity.time}</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Temperature */}
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>Home Temperature</Text>
|
|
<View style={styles.tempRow}>
|
|
<View style={styles.tempItem}>
|
|
<Ionicons name="sunny" size={28} color="#F59E0B" />
|
|
<Text style={styles.tempValue}>{data.temperature.living}°F</Text>
|
|
<Text style={styles.tempLabel}>Living Room</Text>
|
|
</View>
|
|
<View style={styles.tempDivider} />
|
|
<View style={styles.tempItem}>
|
|
<Ionicons name="moon" size={28} color="#6366F1" />
|
|
<Text style={styles.tempValue}>{data.temperature.bedroom}°F</Text>
|
|
<Text style={styles.tempLabel}>Bedroom</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Activities */}
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>Today's Activities</Text>
|
|
<View style={styles.activitiesGrid}>
|
|
{data.activities.map((activity) => (
|
|
<View key={activity.id} style={styles.activityCard}>
|
|
<View style={[styles.activityIcon, { backgroundColor: activity.color + '20' }]}>
|
|
<Ionicons name={activity.icon} size={22} color={activity.color} />
|
|
</View>
|
|
<Text style={styles.activityTitle}>{activity.title}</Text>
|
|
<Text style={styles.activityValue}>{activity.value}</Text>
|
|
<Text style={styles.activityTime}>{activity.time}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Alerts Section */}
|
|
{data.alerts > 0 && (
|
|
<View style={[styles.card, styles.alertCard]}>
|
|
<View style={styles.alertHeader}>
|
|
<Ionicons name="alert-circle" size={24} color={AppColors.warning} />
|
|
<Text style={styles.alertTitle}>Attention Needed</Text>
|
|
</View>
|
|
<Text style={styles.alertText}>
|
|
Lower than usual activity detected. Consider checking in.
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Info Footer */}
|
|
<View style={styles.infoFooter}>
|
|
<Ionicons name="information-circle" size={18} color={AppColors.textMuted} />
|
|
<Text style={styles.infoText}>
|
|
This is demo data. Once sensors are installed and connected,
|
|
you'll see real activity data from your loved one's home.
|
|
</Text>
|
|
</View>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: AppColors.background,
|
|
},
|
|
content: {
|
|
padding: Spacing.md,
|
|
paddingBottom: Spacing.xxl,
|
|
},
|
|
mockBanner: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: `${AppColors.warning}15`,
|
|
paddingVertical: Spacing.sm,
|
|
paddingHorizontal: Spacing.md,
|
|
borderRadius: BorderRadius.md,
|
|
marginBottom: Spacing.md,
|
|
gap: Spacing.xs,
|
|
},
|
|
mockBannerText: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.warning,
|
|
fontWeight: FontWeights.medium,
|
|
},
|
|
card: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.lg,
|
|
marginBottom: Spacing.md,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 1 },
|
|
shadowOpacity: 0.05,
|
|
shadowRadius: 3,
|
|
elevation: 1,
|
|
},
|
|
cardHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: Spacing.md,
|
|
},
|
|
cardTitle: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
badge: {
|
|
paddingHorizontal: Spacing.sm,
|
|
paddingVertical: Spacing.xs,
|
|
borderRadius: BorderRadius.full,
|
|
},
|
|
badgeText: {
|
|
fontSize: FontSizes.xs,
|
|
fontWeight: FontWeights.semibold,
|
|
},
|
|
scoreContainer: {
|
|
alignItems: 'center',
|
|
},
|
|
scoreValue: {
|
|
fontSize: 48,
|
|
fontWeight: FontWeights.bold,
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
scoreBar: {
|
|
width: '100%',
|
|
height: 8,
|
|
backgroundColor: AppColors.border,
|
|
borderRadius: BorderRadius.full,
|
|
overflow: 'hidden',
|
|
},
|
|
scoreProgress: {
|
|
height: '100%',
|
|
borderRadius: BorderRadius.full,
|
|
},
|
|
statusRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
marginTop: Spacing.sm,
|
|
},
|
|
statusItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.sm,
|
|
},
|
|
statusTextContainer: {
|
|
alignItems: 'flex-start',
|
|
},
|
|
statusLabel: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textMuted,
|
|
},
|
|
statusValue: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
tempRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
alignItems: 'center',
|
|
marginTop: Spacing.sm,
|
|
},
|
|
tempItem: {
|
|
alignItems: 'center',
|
|
flex: 1,
|
|
},
|
|
tempDivider: {
|
|
width: 1,
|
|
height: 50,
|
|
backgroundColor: AppColors.border,
|
|
},
|
|
tempValue: {
|
|
fontSize: FontSizes.xl,
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.textPrimary,
|
|
marginTop: Spacing.xs,
|
|
},
|
|
tempLabel: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textMuted,
|
|
marginTop: Spacing.xs,
|
|
},
|
|
activitiesGrid: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
gap: Spacing.sm,
|
|
marginTop: Spacing.sm,
|
|
},
|
|
activityCard: {
|
|
width: '48%',
|
|
backgroundColor: AppColors.background,
|
|
borderRadius: BorderRadius.md,
|
|
padding: Spacing.md,
|
|
alignItems: 'center',
|
|
},
|
|
activityIcon: {
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: BorderRadius.full,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
activityTitle: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
activityValue: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
activityTime: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textMuted,
|
|
marginTop: Spacing.xs,
|
|
},
|
|
alertCard: {
|
|
backgroundColor: `${AppColors.warning}10`,
|
|
borderWidth: 1,
|
|
borderColor: `${AppColors.warning}30`,
|
|
},
|
|
alertHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.sm,
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
alertTitle: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.warning,
|
|
},
|
|
alertText: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textSecondary,
|
|
lineHeight: 20,
|
|
},
|
|
infoFooter: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
gap: Spacing.sm,
|
|
marginTop: Spacing.md,
|
|
paddingHorizontal: Spacing.sm,
|
|
},
|
|
infoText: {
|
|
flex: 1,
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textMuted,
|
|
lineHeight: 18,
|
|
},
|
|
});
|