frontend: display the link of mood and trigger
Parents:
729fa788 file(s) changed
- frontend/app/(tabs)/new/post-mood.tsx +12 -1
- frontend/app/care-actions/post-appointment.tsx +0 -1
- frontend/app/entry/[id].tsx +94 -0
- frontend/app/triggers/[id].tsx +103 -1
- frontend/components/misc/NoLinkMessage.tsx +50 -0
- frontend/hooks/useTrigger.queries.ts +3 -1
- frontend/lib/api/index.ts +3 -0
- frontend/lib/api/types.ts +10 -0
frontend/app/(tabs)/new/post-mood.tsx
1 1
import { useState } from "react";
2 2
import {
3 3
View,
4 4
Text,
5 5
StyleSheet,
6 6
TouchableOpacity,
7 7
Pressable,
8 8
ActivityIndicator,
9 9
} from "react-native";
10 -
import { router, useLocalSearchParams } from "expo-router";
10 +
import { router, useLocalSearchParams, Link } from "expo-router";
11 11
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
12 12
13 13
import { ScreenLayout } from "@/components/ui/ScreenLayout";
14 14
import { Card } from "@/components/ui/Cards";
15 +
import { Section } from '@/components/ui/Sections';
15 16
import { Button } from "@/components/ui/Button";
16 17
import { useAuth } from "@/context/AuthContext";
17 18
import { Colors, Spacing, Typography, BorderRadius, Shadows } from "@/constants/theme";
18 19
import { getTriggerCategory } from "@/constants/trigger-categories";
19 20
import { useTriggers, useLinkMoodToTrigger } from "@/hooks";
▸ 175 unchanged lines
195 196
)}
196 197
</>
197 198
)}
198 199
</Card>
199 200
)}
201 +
202 +
<Button
203 +
title="Pular essa etapa"
204 +
variant="ghost"
205 +
onPress={() => router.replace("/(tabs)/new")}
206 +
style={styles.concludeButton}
207 +
/>
200 208
</ScreenLayout>
201 209
);
202 210
}
203 211
204 212
const styles = StyleSheet.create({
▸ 153 unchanged lines
358 366
fontWeight: "600",
359 367
color: Colors.light.tint,
360 368
},
361 369
moodList: {
362 370
gap: 8,
371 +
},
372 +
concludeButton: {
373 +
marginTop: 8,
363 374
},
364 375
});
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
▸ 70 unchanged lines
76 76
color={Colors.light.disabled}
77 77
/>
78 78
</TouchableOpacity>
79 79
</Card>
80 80
81 -
{/* Concluir ghost button */}
82 81
<Button
83 82
title="Concluir"
84 83
variant="ghost"
85 84
onPress={() => router.replace("/(tabs)/actions")}
86 85
style={styles.concludeButton}
▸ 94 unchanged lines
181 180
},
182 181
concludeButton: {
183 182
marginTop: 8,
184 183
},
185 184
});
frontend/app/entry/[id].tsx
1 1
import { useState, useEffect, use } from "react";
2 2
import {
3 3
StyleSheet,
4 4
Text,
5 5
View,
▸ 4 unchanged lines
10 10
import { useLocalSearchParams, router, Stack } from "expo-router";
11 11
import { format } from "date-fns";
12 12
import { ptBR } from "date-fns/locale";
13 13
14 14
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
15 +
import { NoLinkMessage } from "@/components/misc/NoLinkMessage";
15 16
import { Badge } from "@/components/ui/Badge";
16 17
import { Button } from "@/components/ui/Button";
17 18
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
18 19
import { Section, SectionHeader } from "@/components/ui/Sections";
19 20
import { Theme, Colors, Spacing } from "@/constants/theme";
▸ 4 unchanged lines
24 25
intensityToValue,
25 26
} from "@/constants/mood-components";
26 27
import { useMoodEntry, useDeleteMoodEntry } from "@/hooks";
27 28
import { getTimeBadge, formatMoment } from "@/lib/utils/time";
28 29
import { MoodComponentCard } from "@/components/ui/MoodComponentCard";
30 +
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
31 +
import { getTriggerCategory } from "@/constants/trigger-categories";
32 +
33 +
const impactColor = (n: number) => {
34 +
if (n <= 1) return '#86efac';
35 +
if (n <= 2) return '#22c55e';
36 +
if (n <= 3) return '#f59e0b';
37 +
if (n <= 4) return '#f97316';
38 +
return '#ef4444';
39 +
};
29 40
30 41
export default function MoodDetailScreen() {
31 42
const { id } = useLocalSearchParams<{ id: string }>();
32 43
const { data: entry, isLoading, isError } = useMoodEntry(id);
33 44
const deleteMoodEntry = useDeleteMoodEntry();
▸ 142 unchanged lines
176 187
);
177 188
})}
178 189
</Col>
179 190
</Section>
180 191
192 +
{/* Linked triggers */}
193 +
<Section>
194 +
<SectionHeader title="Vínculo" variant="subtle" />
195 +
{entry.triggerLinks && entry.triggerLinks.length > 0 ? (
196 +
<Col gap={8}>
197 +
{entry.triggerLinks.map((link) => {
198 +
const cat = getTriggerCategory(link.trigger.category);
199 +
return (
200 +
<Pressable
201 +
key={link.id}
202 +
onPress={() => router.push(`/triggers/${link.trigger.id}`)}
203 +
style={({ pressed }) => [pressed && { opacity: 0.7 }]}
204 +
>
205 +
<Card style={styles.linkCard}>
206 +
<Row style={{ alignItems: "center", gap: 12 }}>
207 +
<View style={[styles.linkIconCircle, { backgroundColor: cat.iconBackground }]}>
208 +
<MaterialIcons name={cat.icon} size={22} color={cat.color} />
209 +
</View>
210 +
<Col gap={3} style={{ flex: 1 }}>
211 +
<Row style={{ alignItems: "center", gap: 6 }}>
212 +
<Text style={styles.linkLabel}>{cat.label}</Text>
213 +
<Badge label="Gatilho" variant="orange" style={undefined} />
214 +
</Row>
215 +
<Text style={styles.linkTime}>
216 +
{formatMoment(new Date(link.trigger.moment))}
217 +
</Text>
218 +
</Col>
219 +
<Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
220 +
</Row>
221 +
<View style={styles.impactBar}>
222 +
<View
223 +
style={[
224 +
styles.impactFill,
225 +
{
226 +
width: `${(link.perceivedImpact / 5) * 100}%` as any,
227 +
backgroundColor: impactColor(link.perceivedImpact),
228 +
},
229 +
]}
230 +
/>
231 +
</View>
232 +
</Card>
233 +
</Pressable>
234 +
);
235 +
})}
236 +
</Col>
237 +
) : (
238 +
<NoLinkMessage />
239 +
)}
240 +
</Section>
241 +
181 242
{/* Annotation / notes */}
182 243
{entry.annotation && (
183 244
<Section>
184 245
<SectionHeader title="Anotações" variant="subtle" />
185 246
<Card style={{ padding: Spacing.cardGap }}>
▸ 127 unchanged lines
313 374
lineHeight: 22,
314 375
color: Colors.light.textSecondary,
315 376
fontStyle: "italic",
316 377
padding: 4,
317 378
},
379 +
// Linked trigger cards
380 +
linkCard: {
381 +
padding: Spacing.cardGap,
382 +
overflow: "hidden",
383 +
gap: 10,
384 +
},
385 +
linkIconCircle: {
386 +
width: 44,
387 +
height: 44,
388 +
borderRadius: 22,
389 +
alignItems: "center",
390 +
justifyContent: "center",
391 +
},
392 +
linkTime: {
393 +
fontSize: 12,
394 +
color: Colors.light.textSecondary,
395 +
},
396 +
linkLabel: {
397 +
fontSize: 15,
398 +
fontWeight: "700",
399 +
color: Colors.light.text,
400 +
},
401 +
impactBar: {
402 +
height: 3,
403 +
backgroundColor: Colors.light.divider,
404 +
borderRadius: 99,
405 +
overflow: "hidden",
406 +
},
407 +
impactFill: {
408 +
height: "100%",
409 +
borderRadius: 99,
410 +
},
411 +
318 412
});
frontend/app/triggers/[id].tsx
1 1
import { useState, useEffect, use } from "react";
2 2
import {
3 3
StyleSheet,
4 4
Text,
5 5
View,
▸ 4 unchanged lines
10 10
import { useLocalSearchParams, router, Stack } from "expo-router";
11 11
import { format } from "date-fns";
12 12
import { ptBR } from "date-fns/locale";
13 13
14 14
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
15 +
import { NoLinkMessage } from "@/components/misc/NoLinkMessage";
15 16
import { Badge } from "@/components/ui/Badge";
16 17
import { Button } from "@/components/ui/Button";
17 18
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
18 19
import { Section, SectionHeader } from "@/components/ui/Sections";
19 20
import { Theme, Typography, Colors, Spacing } from "@/constants/theme";
20 -
import { MaterialIcons } from "@expo/vector-icons";
21 +
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
21 22
import { getTriggerCategory } from "@/constants/trigger-categories";
22 23
import {
23 24
useTrigger,
24 25
useDeleteTrigger,
25 26
} from "@/hooks/useTrigger.queries";
26 27
import { formatMoment } from "@/lib/utils/time";
28 +
import { getMood } from "@/constants/moods";
29 +
30 +
const impactColor = (n: number) => {
31 +
if (n <= 1) return '#86efac';
32 +
if (n <= 2) return '#22c55e';
33 +
if (n <= 3) return '#f59e0b';
34 +
if (n <= 4) return '#f97316';
35 +
return '#ef4444';
36 +
};
27 37
28 38
export default function TriggerDetailsScreen() {
29 39
const { id } = useLocalSearchParams<{ id: string }>();
30 40
const { data: trigger, isLoading, isError } = useTrigger(id);
31 41
const deleteTrigger = useDeleteTrigger();
▸ 87 unchanged lines
119 129
</Text>
120 130
</Col>
121 131
</Row>
122 132
</Card>
123 133
134 +
{/* Linked moods */}
135 +
<Section>
136 +
<SectionHeader title="Vínculo" variant="subtle" />
137 +
{trigger.moodLinks && trigger.moodLinks.length > 0 ? (
138 +
<Col gap={8}>
139 +
{trigger.moodLinks.map((link) => {
140 +
const moodDef = getMood(link.mood.selectedMood.toLowerCase());
141 +
return (
142 +
<Pressable
143 +
key={link.id}
144 +
onPress={() => router.push(`/entry/${link.mood.id}`)}
145 +
style={({ pressed }) => [pressed && { opacity: 0.7 }]}
146 +
>
147 +
<Card style={styles.linkCard}>
148 +
<Row style={{ alignItems: "center", gap: 12 }}>
149 +
<View style={styles.linkIconCircle}>
150 +
<Text style={styles.linkMoodEmoji}>{moodDef?.icon ?? "😐"}</Text>
151 +
</View>
152 +
<Col gap={3} style={{ flex: 1 }}>
153 +
<Row style={{ alignItems: "center", gap: 6 }}>
154 +
<Text style={styles.linkLabel}>
155 +
{moodDef?.label ?? link.mood.selectedMood}
156 +
</Text>
157 +
158 +
<Badge
159 +
label="Humor"
160 +
variant="yellow"
161 +
style={undefined}
162 +
/>
163 +
</Row>
164 +
165 +
<Text style={styles.linkTime}>
166 +
{formatMoment(new Date(link.mood.moment))}
167 +
</Text>
168 +
</Col>
169 +
<Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
170 +
</Row>
171 +
<View style={styles.impactBar}>
172 +
<View
173 +
style={[
174 +
styles.impactFill,
175 +
{
176 +
width: `${(link.perceivedImpact / 5) * 100}%`,
177 +
backgroundColor: impactColor(link.perceivedImpact),
178 +
},
179 +
]}
180 +
/>
181 +
</View>
182 +
</Card>
183 +
</Pressable>
184 +
);
185 +
})}
186 +
</Col>
187 +
) : (
188 +
<NoLinkMessage />
189 +
)}
190 +
</Section>
191 +
124 192
{/* Annotation / notes */}
125 193
{trigger.comment && (
126 194
<Section>
127 195
<SectionHeader title="Anotações" variant="subtle" />
128 196
<Card style={{ padding: Spacing.cardGap }}>
▸ 109 unchanged lines
238 306
fontSize: 14,
239 307
lineHeight: 22,
240 308
color: Colors.light.textSecondary,
241 309
fontStyle: "italic",
242 310
padding: 4,
311 +
},
312 +
// Linked mood cards
313 +
linkCard: {
314 +
padding: Spacing.cardGap,
315 +
overflow: "hidden",
316 +
gap: 10,
317 +
},
318 +
linkIconCircle: {
319 +
width: 44,
320 +
height: 44,
321 +
alignItems: "center",
322 +
justifyContent: "center",
323 +
},
324 +
linkMoodEmoji: {
325 +
fontSize: 28,
326 +
},
327 +
linkTime: {
328 +
fontSize: 12,
329 +
color: Colors.light.textSecondary,
330 +
},
331 +
linkLabel: {
332 +
fontSize: 15,
333 +
fontWeight: "700",
334 +
color: Colors.light.text,
335 +
},
336 +
impactBar: {
337 +
height: 3,
338 +
backgroundColor: Colors.light.divider,
339 +
borderRadius: 99,
340 +
overflow: "hidden",
341 +
},
342 +
impactFill: {
343 +
height: "100%",
344 +
borderRadius: 99,
243 345
},
244 346
});
frontend/components/misc/NoLinkMessage.tsx
@@ -0,0 +1,50 @@
1 + import React from "react";
2 + import { View, Text, StyleSheet } from "react-native";
3 + import { Ionicons } from "@expo/vector-icons";
4 + import { Colors, Typography, Spacing } from "@/constants/theme";
5 +
6 + const styles = StyleSheet.create({
7 + container: {
8 + flexDirection: "row",
9 + alignItems: "flex-start",
10 + gap: 8,
11 + paddingHorizontal: 4,
12 + paddingVertical: Spacing.cardGap,
13 + },
14 + text: {
15 + ...Typography.bodyMd,
16 + color: Colors.light.textSecondary,
17 + flex: 1,
18 + lineHeight: 20,
19 + fontStyle: 'italic',
20 + fontSize: 12
21 + },
22 + bold: {
23 + fontWeight: "700"
24 + },
25 + });
26 +
27 + const MESSAGES = {
28 + general: (
29 + <>
30 + Registros também podem ser
31 + <Text style={styles.bold}> Sem vínculo </Text>
32 + ou{" "}
33 + <Text style={styles.bold}>Vinculados a uma ação de cuidado</Text> (ex:
34 + Meditação, Exercício).
35 + </>
36 + ),
37 + };
38 +
39 + interface NoLinkMessageProps {
40 + variant?: keyof typeof MESSAGES;
41 + }
42 +
43 + export function NoLinkMessage({ variant = "general" }: NoLinkMessageProps) {
44 + return (
45 + <View style={styles.container}>
46 + <Ionicons name="information-circle-outline" size={20} color={Colors.light.textSecondary} />
47 + <Text style={styles.text}>{MESSAGES[variant] ?? MESSAGES.general}</Text>
48 + </View>
49 + );
50 + }
frontend/hooks/useTrigger.queries.ts
1 1
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2 2
3 3
import { apiClient, Trigger, CreateTriggerPayload, UpdateTriggerPayload } from "@/lib/api";
4 4
import { insightKeys } from "@/hooks/useInsights.queries";
5 +
import { moodKeys } from "@/hooks/useMoodEntries.queries";
5 6
6 7
export const triggerKeys = {
7 8
all: () => ["triggers"] as const,
8 9
lists: () => [...triggerKeys.all(), "list"] as const,
9 10
list: (filters?: TriggerFilters) =>
▸ 132 unchanged lines
142 143
moodId: string;
143 144
perceivedImpact: number
144 145
}) =>
145 146
apiClient.linkMoodToTrigger(triggerId, { moodId: Number(moodId), perceivedImpact }),
146 147
147 -
onSuccess: (_data, { triggerId }) => {
148 +
onSuccess: (_data, { triggerId, moodId }) => {
148 149
queryClient.invalidateQueries({ queryKey: triggerKeys.detail(triggerId) });
150 +
queryClient.invalidateQueries({ queryKey: moodKeys.detail(moodId) });
149 151
},
150 152
});
151 153
};
152 154
153 155
// Delete a mood entry with optimistic removal
▸ 26 unchanged lines
180 182
queryClient.invalidateQueries({ queryKey: insightKeys.byType("TRIGGER_PATTERN") });
181 183
}, 2000);
182 184
},
183 185
});
184 186
};
frontend/lib/api/index.ts
1 1
export { apiClient } from "@/lib/api/client";
2 2
3 3
export type {
4 4
User,
5 5
AuthResponse,
▸ 25 unchanged lines
31 31
// Medicine Regimens
32 32
MedicineRegimen,
33 33
MedicineRegimenResponse,
34 34
CreateMedicineRegimenPayload,
35 35
UpdateMedicineRegimenPayload,
36 +
UpdateTriggerPayload,
36 37
// Trigger-Mood linking
37 38
TriggerMoodLink,
39 +
TriggerMoodLinkWithMood,
40 +
TriggerMoodLinkWithTrigger,
38 41
LinkMoodPayload,
39 42
} from "@/lib/api/types";
frontend/lib/api/types.ts
1 1
export interface User {
2 2
id: number;
3 3
email: string;
4 4
firstName: string;
5 5
lastName?: string;
▸ 77 unchanged lines
83 83
selectedMood: string;
84 84
anxietyLevel: number;
85 85
energyLevel: number;
86 86
stressLevel: number;
87 87
moodComponents: MoodComponent[];
88 +
triggerLinks?: TriggerMoodLinkWithTrigger[];
88 89
createdAt: Date;
89 90
updatedAt: Date;
90 91
}
91 92
92 93
// Sleep Records
▸ 22 unchanged lines
115 116
comment: string;
116 117
category: string;
117 118
moment: Date;
118 119
createdAt: Date;
119 120
updatedAt: Date;
121 +
moodLinks?: TriggerMoodLinkWithMood[];
120 122
}
121 123
122 124
export interface CreateTriggerPayload {
123 125
comment: string;
124 126
category: string;
▸ 164 unchanged lines
289 291
id: number;
290 292
triggerId: number;
291 293
moodId: number;
292 294
perceivedImpact: number;
293 295
linkedAt: Date;
296 +
}
297 +
298 +
export interface TriggerMoodLinkWithMood extends TriggerMoodLink {
299 +
mood: MoodEntry;
300 +
}
301 +
302 +
export interface TriggerMoodLinkWithTrigger extends TriggerMoodLink {
303 +
trigger: Trigger;
294 304
}
295 305
296 306
// Payloads
297 307
export interface CreateMedicineRegimenPayload {
298 308
name: string;
▸ 43 unchanged lines
342 352
343 353
export interface CareActionResponse { careAction: CareAction; }
344 354
export interface MedicineRegimenResponse { regimen: MedicineRegimen; }
345 355
346 356
export interface LinkMoodPayload { moodId: number; perceivedImpact: number; }