frontend/api: insights UI improvements and post-mood bug fixes
Add a small description to the insights page, display calculation for each insights, fix trigger-mood linkage by making it idempotent: return the existing link instead of throwing a plain Error (which fell through to a 500), so duplicate link attempts are a no-op I still feel this is rather brittle, need to test more
Parents:
60f9bbf8 file(s) changed
- api/src/services/triggerMoodLink.service.ts +1 -1
- frontend/app/(tabs)/insights.tsx +1 -1
- frontend/app/(tabs)/new/entry.tsx +1 -1
- frontend/app/care-actions/post-appointment.tsx +1 -1
- frontend/components/insights/EnergySleepWidget.tsx +7 -5
- frontend/components/insights/InsightCard.tsx +20 -0
- frontend/components/insights/MoodTrendWidget.tsx +1 -0
- frontend/components/insights/TriggerPatternWidget.tsx +1 -0
api/src/services/triggerMoodLink.service.ts
1 1
import { prisma } from '@app/lib/prisma';
2 2
import { TriggerMoodLink } from '@prisma/client';
3 3
4 4
export const linkTriggerToMood = async (
5 5
triggerId: number,
▸ 5 unchanged lines
11 11
triggerId_moodId: { triggerId, moodId },
12 12
},
13 13
});
14 14
15 15
if (existing) {
16 -
throw new Error(`Link between trigger ${triggerId} and mood ${moodId} already exists`);
16 +
return existing;
17 17
}
18 18
19 19
return prisma.triggerMoodLink.create({
20 20
data: { triggerId, moodId, perceivedImpact },
21 21
});
▸ 7 unchanged lines
29 29
where: {
30 30
triggerId_moodId: { triggerId, moodId },
31 31
},
32 32
});
33 33
};
frontend/app/(tabs)/insights.tsx
1 1
import { View, Text, ScrollView, StyleSheet } from "react-native";
2 2
3 3
import { ThemedText } from "@/components/misc/themed-text";
4 4
import { ThemedView } from "@/components/misc/themed-view";
5 5
import { ScreenLayout } from "@/components/ui/ScreenLayout";
▸ 24 unchanged lines
30 30
{ label: "Humor Estável", value: "92%" },
31 31
]}
32 32
/>
33 33
34 34
<Section>
35 -
<SectionHeader title="Indicadores" />
35 +
<SectionHeader title="Indicadores" subtitle="Baseado nos últimos 7 dias" />
36 36
37 37
<EnergySleepWidget />
38 38
39 39
<MoodTrendWidget />
40 40
▸ 90 unchanged lines
131 131
insightMetric: {
132 132
...Typography.headlineLg,
133 133
fontSize: 22,
134 134
},
135 135
});
frontend/app/(tabs)/new/entry.tsx
1 1
import { useState, useEffect } from "react";
2 2
import { Link, router, useLocalSearchParams } from "expo-router";
3 3
4 4
import { View, Text, StyleSheet, ScrollView } from "react-native";
5 5
▸ 35 unchanged lines
41 41
42 42
const createMoodEntry = useCreateMoodEntry();
43 43
const patchCareAction = usePatchCareAction();
44 44
45 45
useEffect(() => {
46 -
setSelectedMood(getMood(initialMood));
46 +
if (initialMood) setSelectedMood(getMood(initialMood));
47 47
}, [initialMood]);
48 48
49 49
useEffect(() => {
50 50
return () => reset();
51 51
}, []);
▸ 157 unchanged lines
209 209
},
210 210
resumeCard: {
211 211
padding: Spacing.cardGap,
212 212
},
213 213
});
frontend/app/care-actions/post-appointment.tsx
1 1
import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from "react-native";
2 2
import { router } from "expo-router";
3 3
import { Ionicons } from "@expo/vector-icons";
4 4
import { SafeAreaView } from "react-native-safe-area-context";
5 5
▸ 52 unchanged lines
58 58
</TouchableOpacity>
59 59
60 60
<TouchableOpacity
61 61
style={styles.optionRow}
62 62
activeOpacity={0.7}
63 -
onPress={() => router.replace("/new/entry")}
63 +
onPress={() => router.replace("/new/entry?initialMood=neutral")}
64 64
>
65 65
<View style={[styles.optionIcon, { backgroundColor: "#fef9c3" }]}>
66 66
<Ionicons name="heart-outline" size={22} color="#ca8a04" />
67 67
</View>
68 68
<View style={styles.optionText}>
▸ 115 unchanged lines
184 184
},
185 185
concludeButton: {
186 186
marginTop: 8,
187 187
},
188 188
});
frontend/components/insights/EnergySleepWidget.tsx
1 1
import React from "react";
2 2
import { Text, StyleSheet, ActivityIndicator } from "react-native";
3 3
import { Card } from "@/components/ui/Cards";
4 4
import { ThemedText } from "@/components/misc/themed-text";
5 5
import { useInsight } from "@/hooks/useInsights.queries";
▸ 33 unchanged lines
39 39
<InsightCard
40 40
category="Energia"
41 41
body={insight.body}
42 42
metric={label}
43 43
accent={accent}
44 +
footnote="Correlação de Pearson entre horas de sono e energia no dia seguinte."
44 45
/>
45 46
);
46 47
};
47 48
48 49
function resolveCorrelation(
49 50
score: number,
50 51
sampleSize: number,
51 52
): { label: string; accent: MetricAccent } {
52 -
const pct = Math.round(Math.abs(score) * 100);
53 53
const lowData = sampleSize < 5;
54 +
const suffix = lowData ? "*" : "";
54 55
55 56
if (score > 0.6)
56 -
return { label: lowData ? "Alta*" : `+${pct}%`, accent: "green" };
57 +
return { label: `Forte${suffix}`, accent: "green" };
57 58
if (score > 0.3)
58 -
return { label: lowData ? "Média*" : `+${pct}%`, accent: "green" };
59 +
return { label: `Moderada${suffix}`, accent: "green" };
59 60
if (score >= -0.3)
60 -
return { label: "~0%", accent: "neutral" };
61 -
return { label: `-${pct}%`, accent: "purple" };
61 +
return { label: `Nenhuma${suffix}`, accent: "neutral" };
62 +
63 +
return { label: `—`, accent: "purple" };
62 64
}
63 65
64 66
const styles = StyleSheet.create({
65 67
container: {
66 68
flex: 1,
▸ 6 unchanged lines
73 75
empty: {
74 76
color: Colors.light.lightSlateGray,
75 77
marginTop: 4,
76 78
},
77 79
});
frontend/components/insights/InsightCard.tsx
1 1
import React from "react";
2 2
import { View, Text, StyleSheet } from "react-native";
3 3
4 4
import { Card } from "@/components/ui/Cards";
5 5
import { Colors, Spacing, Typography } from "@/constants/theme";
▸ 9 unchanged lines
15 15
interface InsightCardProps {
16 16
category: string;
17 17
body: React.ReactNode;
18 18
metric: string;
19 19
accent?: MetricAccent;
20 +
footnote?: string;
20 21
}
21 22
22 23
export const InsightCard: React.FC<InsightCardProps> = ({
23 24
category,
24 25
body,
25 26
metric,
26 27
accent = "neutral",
28 +
footnote,
27 29
}) => (
28 30
<Card style={styles.cardContainer}>
29 31
<View style={styles.inner}>
30 32
<View style={styles.left}>
31 33
<Text style={styles.category}>{category}</Text>
▸ 5 unchanged lines
37 39
</View>
38 40
<Text style={[styles.metric, { color: metricColor[accent] }]}>
39 41
{metric}
40 42
</Text>
41 43
</View>
44 +
{footnote && (
45 +
<>
46 +
<View style={styles.divider} />
47 +
<Text style={styles.footnote}>{footnote}</Text>
48 +
</>
49 +
)}
42 50
</Card>
43 51
);
44 52
45 53
interface HighlightProps {
46 54
children: React.ReactNode;
▸ 31 unchanged lines
78 86
color: Colors.light.text,
79 87
},
80 88
metric: {
81 89
...Typography.headlineLg,
82 90
fontSize: 22,
91 +
},
92 +
divider: {
93 +
height: StyleSheet.hairlineWidth,
94 +
backgroundColor: Colors.light.divider,
95 +
marginTop: Spacing.cardGap,
96 +
marginBottom: 8,
97 +
},
98 +
footnote: {
99 +
fontSize: 11,
100 +
lineHeight: 15,
101 +
color: Colors.light.textSecondary,
102 +
fontStyle: "italic",
83 103
},
84 104
});
frontend/components/insights/MoodTrendWidget.tsx
1 1
import React from "react";
2 2
import { Text, StyleSheet, ActivityIndicator } from "react-native";
3 3
import { Card } from "@/components/ui/Cards";
4 4
import { useInsight } from "@/hooks/useInsights.queries";
5 5
import { Colors, Spacing, Typography } from "@/constants/theme";
▸ 44 unchanged lines
50 50
<InsightCard
51 51
category="Humor"
52 52
body={insight.body}
53 53
metric={metric}
54 54
accent={accent}
55 +
footnote="Compara a média de humor da primeira e segunda metade da semana (escala 1–5)."
55 56
/>
56 57
);
57 58
};
58 59
59 60
const styles = StyleSheet.create({
▸ 23 unchanged lines
83 84
empty: {
84 85
color: Colors.light.lightSlateGray,
85 86
marginTop: 4,
86 87
},
87 88
});
frontend/components/insights/TriggerPatternWidget.tsx
1 1
import React from "react";
2 2
import { Text, View, StyleSheet, ActivityIndicator } from "react-native";
3 3
import { MaterialIcons } from "@expo/vector-icons";
4 4
import { Card } from "@/components/ui/Cards";
5 5
import { useInsight } from "@/hooks/useInsights.queries";
▸ 49 unchanged lines
55 55
<Text style={styles.bodyText}>{insight.body}</Text>
56 56
</View>
57 57
}
58 58
metric={`${pct}%`}
59 59
accent="neutral"
60 +
footnote="Percentual da categoria de gatilho mais frequente no período."
60 61
/>
61 62
);
62 63
};
63 64
64 65
const styles = StyleSheet.create({
▸ 21 unchanged lines
86 87
empty: {
87 88
color: Colors.light.lightSlateGray,
88 89
marginTop: 4,
89 90
},
90 91
});