feat(medications): 添加用药管理功能

- 新增用药标签页,包含完整的用药记录界面
- 实现用药卡片组件,支持状态显示(已服用/未服用/已错过)
- 增强日期选择器,添加"回到今天"快捷功能
- 添加用药相关的图标支持(pills.fill, plus)
- 集成用药路由配置,支持标签页导航

该功能为用户提供完整的用药管理体验,包括用药记录、状态跟踪和日期筛选等核心功能。
This commit is contained in:
richarjiang
2025-11-06 17:51:06 +08:00
parent a228280ca4
commit 3aafc50702
6 changed files with 798 additions and 10 deletions

View File

@@ -21,6 +21,7 @@ type TabConfig = {
const TAB_CONFIGS: Record<string, TabConfig> = {
statistics: { icon: 'chart.pie.fill', title: '健康' },
medications: { icon: 'pills.fill', title: '用药' },
fasting: { icon: 'timer', title: '断食' },
challenges: { icon: 'trophy.fill', title: '挑战' },
personal: { icon: 'person.fill', title: '个人' },
@@ -36,6 +37,7 @@ export default function TabLayout() {
const isTabSelected = (routeName: string): boolean => {
const routeMap: Record<string, string> = {
statistics: ROUTES.TAB_STATISTICS,
medications: ROUTES.TAB_MEDICATIONS,
fasting: ROUTES.TAB_FASTING,
challenges: ROUTES.TAB_CHALLENGES,
personal: ROUTES.TAB_PERSONAL,
@@ -176,6 +178,10 @@ export default function TabLayout() {
<Label></Label>
<Icon sf="chart.pie.fill" drawable="custom_android_drawable" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name="medications">
<Icon sf="pills.fill" drawable="custom_android_drawable" />
<Label></Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="fasting">
<Icon sf="timer" drawable="custom_android_drawable" />
<Label></Label>
@@ -198,6 +204,7 @@ export default function TabLayout() {
>
<Tabs.Screen name="statistics" options={{ title: '健康' }} />
<Tabs.Screen name="medications" options={{ title: '用药' }} />
<Tabs.Screen name="fasting" options={{ title: '断食' }} />
<Tabs.Screen name="challenges" options={{ title: '挑战' }} />
<Tabs.Screen name="personal" options={{ title: '个人' }} />

423
app/(tabs)/medications.tsx Normal file
View File

@@ -0,0 +1,423 @@
import { DateSelector } from '@/components/DateSelector';
import { MedicationCard, type Medication, type MedicationStatus } from '@/components/medication/MedicationCard';
import { ThemedText } from '@/components/ThemedText';
import { IconSymbol } from '@/components/ui/IconSymbol';
import { Colors } from '@/constants/Colors';
import { useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { DEFAULT_MEMBER_NAME } from '@/store/userSlice';
import dayjs, { Dayjs } from 'dayjs';
import 'dayjs/locale/zh-cn';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useMemo, useState } from 'react';
import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
dayjs.locale('zh-cn');
type MedicationFilter = 'all' | 'taken' | 'missed';
type ThemeColors = (typeof Colors)[keyof typeof Colors];
const MEDICATION_IMAGES = {
bottle: require('@/assets/images/icons/icon-healthy-diet.png'),
drops: require('@/assets/images/icons/icon-remind.png'),
vitamins: require('@/assets/images/icons/icon-blood-oxygen.png'),
};
export default function MedicationsScreen() {
const insets = useSafeAreaInsets();
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colors: ThemeColors = Colors[theme];
const userProfile = useAppSelector((state) => state.user.profile);
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
const [selectedDateIndex, setSelectedDateIndex] = useState<number>(selectedDate.date() - 1);
const [activeFilter, setActiveFilter] = useState<MedicationFilter>('all');
const scheduledMedications = useMemo(() => {
const today = dayjs();
const todayKey = today.format('YYYY-MM-DD');
const yesterdayKey = today.subtract(1, 'day').format('YYYY-MM-DD');
const twoDaysAgoKey = today.subtract(2, 'day').format('YYYY-MM-DD');
return {
[todayKey]: [
{
id: 'med-1',
name: 'Metformin',
dosage: '1 粒胶囊',
scheduledTime: '09:00',
frequency: '每日',
status: 'upcoming' as MedicationStatus,
image: MEDICATION_IMAGES.bottle,
},
{
id: 'med-2',
name: 'Captopril',
dosage: '2 粒胶囊',
scheduledTime: '20:00',
frequency: '每日',
status: 'upcoming' as MedicationStatus,
image: MEDICATION_IMAGES.vitamins,
},
{
id: 'med-3',
name: 'B 12',
dosage: '1 次注射',
scheduledTime: '22:00',
frequency: '每日',
status: 'taken' as MedicationStatus,
image: MEDICATION_IMAGES.vitamins,
},
{
id: 'med-4',
name: 'I-DROP MGD',
dosage: '2 滴',
scheduledTime: '22:00',
frequency: '每日',
status: 'missed' as MedicationStatus,
image: MEDICATION_IMAGES.drops,
},
{
id: 'med-5',
name: 'Niacin',
dosage: '0.5 片',
scheduledTime: '22:00',
frequency: '每日',
status: 'missed' as MedicationStatus,
image: MEDICATION_IMAGES.bottle,
},
],
[yesterdayKey]: [
{
id: 'med-6',
name: 'B 12',
dosage: '1 次注射',
scheduledTime: '22:00',
frequency: '每日',
status: 'taken' as MedicationStatus,
image: MEDICATION_IMAGES.vitamins,
},
],
[twoDaysAgoKey]: [
{
id: 'med-7',
name: 'I-DROP MGD',
dosage: '2 滴',
scheduledTime: '22:00',
frequency: '每日',
status: 'missed' as MedicationStatus,
image: MEDICATION_IMAGES.drops,
},
{
id: 'med-8',
name: 'Niacin',
dosage: '0.5 片',
scheduledTime: '22:00',
frequency: '每日',
status: 'missed' as MedicationStatus,
image: MEDICATION_IMAGES.bottle,
},
],
} as Record<string, Medication[]>;
}, []);
useEffect(() => {
setActiveFilter('all');
}, [selectedDate]);
const selectedKey = selectedDate.format('YYYY-MM-DD');
const medicationsForDay = scheduledMedications[selectedKey] ?? [];
const filteredMedications = useMemo(() => {
if (activeFilter === 'all') {
return medicationsForDay;
}
return medicationsForDay.filter((item) => item.status === activeFilter);
}, [activeFilter, medicationsForDay]);
const counts = useMemo(() => {
const taken = medicationsForDay.filter((item) => item.status === 'taken').length;
const missed = medicationsForDay.filter((item) => item.status === 'missed').length;
return {
all: medicationsForDay.length,
taken,
missed,
};
}, [medicationsForDay]);
const displayName = userProfile.name?.trim() || DEFAULT_MEMBER_NAME;
const headerDateLabel = selectedDate.isSame(dayjs(), 'day')
? `今天,${selectedDate.format('M月D日')}`
: selectedDate.format('M月D日 dddd');
const emptyState = filteredMedications.length === 0;
return (
<View style={styles.container}>
{/* 背景渐变 */}
<LinearGradient
colors={['#f5e5fbff', '#edf4f4ff', '#f7f8f8ff']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
{/* 装饰性圆圈 */}
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
<ScrollView
contentContainerStyle={[
styles.scrollContent,
{ paddingTop: insets.top + 24, paddingBottom: insets.bottom + 36 },
]}
showsVerticalScrollIndicator={false}
>
<View style={styles.header}>
<View>
<ThemedText style={styles.greeting}>{displayName}</ThemedText>
<ThemedText style={[styles.welcome, { color: colors.textMuted }]}>
</ThemedText>
</View>
</View>
<View style={styles.sectionSpacing}>
<DateSelector
selectedIndex={selectedDateIndex}
onDateSelect={(index, date) => {
setSelectedDate(dayjs(date));
setSelectedDateIndex(index);
}}
disableFutureDates
containerStyle={styles.dateSelectorContainer}
/>
</View>
<View style={styles.sectionSpacing}>
<ThemedText style={styles.sectionHeader}></ThemedText>
<View style={[styles.segmentedControl, { backgroundColor: colors.surface }]}>
{(['all', 'taken', 'missed'] as MedicationFilter[]).map((filter) => {
const isActive = activeFilter === filter;
const labelMap: Record<MedicationFilter, string> = {
all: '全部',
taken: '已服用',
missed: '未服用',
};
return (
<TouchableOpacity
key={filter}
onPress={() => setActiveFilter(filter)}
style={[
styles.segment,
isActive && { backgroundColor: colors.primary },
]}
>
<ThemedText
style={[
styles.segmentLabel,
{ color: isActive ? colors.onPrimary : colors.textSecondary },
]}
>
{labelMap[filter]}
</ThemedText>
<View
style={[
styles.segmentBadge,
{
backgroundColor: isActive ? colors.onPrimary : `${colors.primary}20`,
},
]}
>
<ThemedText
style={[
styles.segmentBadgeText,
{ color: isActive ? colors.primary : colors.textSecondary },
]}
>
{counts[filter]}
</ThemedText>
</View>
</TouchableOpacity>
);
})}
</View>
</View>
{emptyState ? (
<View style={[styles.emptyState, { backgroundColor: colors.surface }]}>
<Image
source={require('@/assets/images/task/ImageEmpty.png')}
style={styles.emptyIllustration}
contentFit="cover"
/>
<ThemedText style={styles.emptyTitle}></ThemedText>
<ThemedText style={[styles.emptySubtitle, { color: colors.textMuted }]}>
</ThemedText>
<TouchableOpacity
style={[styles.primaryButton, { backgroundColor: colors.primary }]}
>
<IconSymbol name="plus" size={18} color={colors.onPrimary} />
<ThemedText style={[styles.primaryButtonText, { color: colors.onPrimary }]}>
</ThemedText>
</TouchableOpacity>
</View>
) : (
<View style={styles.cardsWrapper}>
{filteredMedications.map((item) => (
<MedicationCard
key={item.id}
medication={item}
colors={colors}
selectedDate={selectedDate}
/>
))}
</View>
)}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
decorativeCircle1: {
position: 'absolute',
top: 40,
right: 20,
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#0EA5E9',
opacity: 0.1,
},
decorativeCircle2: {
position: 'absolute',
bottom: -15,
left: -15,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#0EA5E9',
opacity: 0.05,
},
scrollContent: {
paddingHorizontal: 20,
gap: 24,
},
header: {
flexDirection: 'row',
alignItems: 'center',
gap: 16,
},
avatar: {
width: 60,
height: 60,
borderRadius: 30,
},
greeting: {
fontSize: 24,
fontWeight: '600',
},
welcome: {
marginTop: 6,
fontSize: 14,
},
sectionSpacing: {
gap: 16,
},
dateSelectorContainer: {
paddingRight: 0,
},
sectionTitle: {
fontSize: 16,
fontWeight: '500',
},
sectionHeader: {
fontSize: 20,
fontWeight: '600',
},
segmentedControl: {
flexDirection: 'row',
borderRadius: 18,
padding: 6,
gap: 6,
},
segment: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
borderRadius: 14,
paddingVertical: 10,
},
segmentLabel: {
fontSize: 14,
fontWeight: '600',
},
segmentBadge: {
minWidth: 24,
height: 24,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 6,
},
segmentBadgeText: {
fontSize: 12,
fontWeight: '600',
},
emptyState: {
alignItems: 'center',
paddingHorizontal: 24,
paddingVertical: 32,
borderRadius: 24,
gap: 16,
},
emptyIllustration: {
width: 160,
height: 160,
resizeMode: 'contain',
},
emptyTitle: {
textAlign: 'center',
fontSize: 18,
fontWeight: '600',
},
emptySubtitle: {
textAlign: 'center',
fontSize: 14,
lineHeight: 20,
},
primaryButton: {
marginTop: 8,
paddingVertical: 14,
paddingHorizontal: 32,
borderRadius: 22,
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
primaryButtonText: {
fontSize: 16,
fontWeight: '600',
},
cardsWrapper: {
gap: 16,
},
});

View File

@@ -58,6 +58,17 @@ export const DateSelector: React.FC<DateSelectorProps> = ({
const days = getMonthDaysZh(currentMonth);
const monthTitle = externalMonthTitle ?? getMonthTitleZh(currentMonth);
// 判断当前选中的日期是否是今天
const isSelectedDateToday = () => {
const today = dayjs();
const selectedDate = days[selectedIndex]?.date;
if (!selectedDate) return false;
// 检查是否是同一天且在同一个月
return selectedDate.isSame(today, 'day') && currentMonth.isSame(today, 'month');
};
// 滚动相关
const daysScrollRef = useRef<ScrollView | null>(null);
const [scrollWidth, setScrollWidth] = useState(0);
@@ -191,20 +202,47 @@ export const DateSelector: React.FC<DateSelectorProps> = ({
}
};
const handleGoToday = () => {
const today = dayjs();
setCurrentMonth(today);
const todayDays = getMonthDaysZh(today);
const newSelectedIndex = todayDays.findIndex(day => day.dayOfMonth === today.date());
if (newSelectedIndex !== -1) {
if (externalSelectedIndex === undefined) {
setInternalSelectedIndex(newSelectedIndex);
}
const todayDate = today.toDate();
setPickerDate(todayDate);
onDateSelect?.(newSelectedIndex, todayDate);
}
};
return (
<View style={[styles.container, containerStyle]}>
{showMonthTitle && (
<View style={styles.monthTitleContainer}>
<Text style={styles.monthTitle}>{monthTitle}</Text>
{showCalendarIcon && (
<TouchableOpacity
onPress={openDatePicker}
style={styles.calendarIconButton}
activeOpacity={0.7}
>
<Ionicons name="calendar-outline" size={14} color="#666666" />
</TouchableOpacity>
)}
<View style={styles.monthActions}>
{!isSelectedDateToday() && (
<TouchableOpacity
onPress={handleGoToday}
style={styles.todayButton}
activeOpacity={0.8}
>
<Text style={styles.todayButtonText}></Text>
</TouchableOpacity>
)}
{showCalendarIcon && (
<TouchableOpacity
onPress={openDatePicker}
style={styles.calendarIconButton}
activeOpacity={0.7}
>
<Ionicons name="calendar-outline" size={14} color="#666666" />
</TouchableOpacity>
)}
</View>
</View>
)}
@@ -305,9 +343,13 @@ const styles = StyleSheet.create({
monthTitleContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
justifyContent: 'space-between',
marginBottom: 8,
},
monthActions: {
flexDirection: 'row',
alignItems: 'center',
},
monthTitle: {
fontSize: 20,
fontWeight: '800',
@@ -318,6 +360,18 @@ const styles = StyleSheet.create({
borderRadius: 6,
marginLeft: 4
},
todayButton: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
backgroundColor: '#EEF2FF',
marginRight: 8,
},
todayButtonText: {
fontSize: 12,
fontWeight: '600',
color: '#4C1D95',
},
daysContainer: {
paddingBottom: 8,
},

View File

@@ -0,0 +1,301 @@
import { ThemedText } from '@/components/ThemedText';
import { Ionicons } from '@expo/vector-icons';
import dayjs, { Dayjs } from 'dayjs';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { Image } from 'expo-image';
import React from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
export type MedicationStatus = 'upcoming' | 'taken' | 'missed';
export type Medication = {
id: string;
name: string;
dosage: string;
scheduledTime: string;
frequency: string;
status: MedicationStatus;
image: any;
};
export type MedicationCardProps = {
medication: Medication;
colors: (typeof import('@/constants/Colors').Colors)[keyof typeof import('@/constants/Colors').Colors];
selectedDate: Dayjs;
};
export function MedicationCard({ medication, colors, selectedDate }: MedicationCardProps) {
const scheduledDate = dayjs(`${selectedDate.format('YYYY-MM-DD')} ${medication.scheduledTime}`);
const timeDiffMinutes = scheduledDate.diff(dayjs(), 'minute');
const renderStatusBadge = () => {
if (medication.status === 'missed') {
return (
<View style={[styles.statusChip, styles.statusChipMissed]}>
<ThemedText style={styles.statusChipText}></ThemedText>
</View>
);
}
if (medication.status === 'upcoming') {
if (timeDiffMinutes <= 0) {
return (
<View style={[styles.statusChip, styles.statusChipUpcoming]}>
<Ionicons name="time-outline" size={14} color="#fff" />
<ThemedText style={styles.statusChipText}></ThemedText>
</View>
);
}
const hours = Math.floor(timeDiffMinutes / 60);
const minutes = timeDiffMinutes % 60;
const formatted =
hours > 0 ? `${hours}小时${minutes > 0 ? `${minutes}分钟` : ''}` : `${minutes}分钟`;
return (
<View style={[styles.statusChip, styles.statusChipUpcoming]}>
<Ionicons name="time-outline" size={14} color="#fff" />
<ThemedText style={styles.statusChipText}> {formatted}</ThemedText>
</View>
);
}
return null;
};
const renderAction = () => {
if (medication.status === 'taken') {
return (
<View style={[styles.actionButton, styles.actionButtonTaken]}>
<Ionicons name="checkmark-circle" size={18} color="#fff" />
<ThemedText style={styles.actionButtonText}></ThemedText>
</View>
);
}
if (medication.status === 'missed') {
return (
<TouchableOpacity
activeOpacity={1}
disabled={true}
onPress={() => {
// 已错过的药物不能服用
console.log('已错过的药物不能服用');
}}
>
{isLiquidGlassAvailable() ? (
<GlassView
style={[styles.actionButton, styles.actionButtonMissed]}
glassEffectStyle="clear"
tintColor="rgba(156, 163, 175, 0.3)"
isInteractive={false}
>
<ThemedText style={styles.actionButtonTextMissed}></ThemedText>
</GlassView>
) : (
<View style={[styles.actionButton, styles.actionButtonMissed, styles.fallbackActionButtonMissed]}>
<ThemedText style={styles.actionButtonTextMissed}></ThemedText>
</View>
)}
</TouchableOpacity>
);
}
return (
<TouchableOpacity
activeOpacity={0.7}
onPress={() => {
// TODO: 实现服药功能
console.log('服药功能待实现');
}}
>
{isLiquidGlassAvailable() ? (
<GlassView
style={[styles.actionButton, styles.actionButtonUpcoming]}
glassEffectStyle="clear"
tintColor="rgba(19, 99, 255, 0.3)"
isInteractive={true}
>
<ThemedText style={styles.actionButtonText}></ThemedText>
</GlassView>
) : (
<View style={[styles.actionButton, styles.actionButtonUpcoming, styles.fallbackActionButton]}>
<ThemedText style={styles.actionButtonText}></ThemedText>
</View>
)}
</TouchableOpacity>
);
};
const statusChip = renderStatusBadge();
return (
<View style={[styles.card, { shadowColor: colors.text }]}>
<View style={[styles.cardSurface, { backgroundColor: colors.surface }]}>
{statusChip ? <View style={styles.statusChipWrapper}>{statusChip}</View> : null}
<View style={styles.cardBody}>
<View style={styles.cardContent}>
<View style={styles.thumbnailWrapper}>
<View style={styles.thumbnailSurface}>
<Image source={medication.image} style={styles.thumbnailImage} />
</View>
</View>
<View style={styles.infoSection}>
<ThemedText style={[styles.cardTitle, { color: colors.text }]}>
{medication.name}
</ThemedText>
<ThemedText style={[styles.cardDosage, { color: colors.textSecondary }]}>
{medication.dosage}
</ThemedText>
<View style={styles.scheduleRow}>
<Ionicons
name="time-outline"
size={14}
color={colors.textSecondary}
style={styles.scheduleIcon}
/>
<ThemedText style={[styles.cardSchedule, { color: colors.textSecondary }]}>
{medication.scheduledTime} | {medication.frequency}
</ThemedText>
</View>
<View style={styles.actionContainer}>{renderAction()}</View>
</View>
</View>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
borderRadius: 26,
shadowOpacity: 0.08,
shadowOffset: { width: 0, height: 12 },
shadowRadius: 24,
elevation: 2,
position: 'relative',
},
cardSurface: {
borderRadius: 26,
overflow: 'hidden',
},
cardBody: {
paddingHorizontal: 20,
paddingBottom: 20,
paddingTop: 28,
},
cardContent: {
flexDirection: 'row',
alignItems: 'center',
gap: 20,
},
thumbnailWrapper: {
width: 126,
height: 110,
},
thumbnailSurface: {
flex: 1,
borderRadius: 22,
backgroundColor: '#F1F4FF',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
thumbnailImage: {
width: '80%',
height: '80%',
resizeMode: 'contain',
},
infoSection: {
flex: 1,
},
cardTitle: {
fontSize: 16,
fontWeight: '700',
},
cardDosage: {
fontSize: 12,
marginTop: 4,
},
scheduleRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
cardSchedule: {
fontSize: 12,
},
scheduleIcon: {
marginTop: -1,
},
actionContainer: {
marginTop: 8,
},
actionButton: {
alignSelf: 'stretch',
flexDirection: 'row',
alignItems: 'center',
gap: 6,
justifyContent: 'center',
height: 38,
borderRadius: 24,
overflow: 'hidden',
},
actionButtonUpcoming: {
backgroundColor: '#1363FF',
},
actionButtonTaken: {
backgroundColor: '#1FBF4B',
},
actionButtonMissed: {
backgroundColor: '#9CA3AF',
},
fallbackActionButton: {
borderWidth: 1,
borderColor: 'rgba(19, 99, 255, 0.3)',
backgroundColor: 'rgba(19, 99, 255, 0.9)',
},
fallbackActionButtonMissed: {
borderWidth: 1,
borderColor: 'rgba(156, 163, 175, 0.3)',
backgroundColor: 'rgba(156, 163, 175, 0.9)',
},
actionButtonText: {
fontSize: 14,
fontWeight: '700',
color: '#fff',
},
actionButtonTextMissed: {
fontSize: 14,
fontWeight: '700',
color: '#fff',
},
statusChipWrapper: {
position: 'absolute',
top: 0,
right: 0,
},
statusChip: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
height: 28,
borderBottomLeftRadius: 20,
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
backgroundColor: '#1363FF',
},
statusChipUpcoming: {
backgroundColor: '#1363FF',
},
statusChipMissed: {
backgroundColor: '#FF3B30',
},
statusChipText: {
fontSize: 10,
fontWeight: '600',
color: '#fff',
},
});

View File

@@ -23,6 +23,8 @@ const MAPPING = {
'trophy.fill': 'emoji-events',
'timer': 'timer',
'person.fill': 'person',
'plus': 'add',
'pills.fill': 'medication',
'person.3.fill': 'people',
'message.fill': 'message',
'info.circle': 'info',

View File

@@ -4,6 +4,7 @@ export const ROUTES = {
TAB_EXPLORE: '/explore',
TAB_COACH: '/coach',
TAB_STATISTICS: '/statistics',
TAB_MEDICATIONS: '/medications',
TAB_CHALLENGES: '/challenges',
TAB_PERSONAL: '/personal',
TAB_FASTING: '/fasting',