frontend: display any links for triggers or mood entries
Parents:
fa185352 file(s) changed
- frontend/app/entry/[id].tsx +37 -1
- frontend/app/triggers/[id].tsx +94 -2
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,
▸ 21 unchanged lines
27 27
import { useMoodEntry, useDeleteMoodEntry } from "@/hooks";
28 28
import { getTimeBadge, formatMoment } from "@/lib/utils/time";
29 29
import { MoodComponentCard } from "@/components/ui/MoodComponentCard";
30 30
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
31 31
import { getTriggerCategory } from "@/constants/trigger-categories";
32 +
import { getCareActionMeta } from "@/constants/care-action-categories";
32 33
33 34
const impactColor = (n: number) => {
34 35
if (n <= 1) return '#86efac';
35 36
if (n <= 2) return '#22c55e';
36 37
if (n <= 3) return '#f59e0b';
▸ 66 unchanged lines
103 104
const timeLeftStr = `(Disponível por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
104 105
const suffix = canDelete ? timeLeftStr : "(Expirado)";
105 106
106 107
return `${msg} ${suffix}`;
107 108
};
109 +
110 +
const hasAnyLinks = (
111 +
entry.triggerLinks && entry.triggerLinks.length > 0
112 +
) || (entry.careActions && entry.careActions.length > 0);
108 113
109 114
return (
110 115
<>
111 116
<Stack.Screen options={{ title: "Ver detalhes" }} />
112 117
▸ 77 unchanged lines
190 195
</Section>
191 196
192 197
{/* Linked triggers */}
193 198
<Section>
194 199
<SectionHeader title="Vínculo" variant="subtle" />
195 -
{entry.triggerLinks && entry.triggerLinks.length > 0 ? (
200 +
{!!hasAnyLinks ? (
196 201
<Col gap={8}>
197 202
{entry.triggerLinks.map((link) => {
198 203
const cat = getTriggerCategory(link.trigger.category);
199 204
return (
200 205
<Pressable
▸ 26 unchanged lines
227 232
backgroundColor: impactColor(link.perceivedImpact),
228 233
},
229 234
]}
230 235
/>
231 236
</View>
237 +
</Card>
238 +
</Pressable>
239 +
);
240 +
})}
241 +
242 +
{entry.careActions.map((ca) => {
243 +
const meta = getCareActionMeta(ca);
244 +
245 +
return (
246 +
<Pressable
247 +
key={ca.id}
248 +
onPress={() => (router.push as any)(`/care-actions/${ca.id}`)}
249 +
style={({ pressed }) => [pressed && { opacity: 0.7 }]}
250 +
>
251 +
<Card style={styles.linkCard}>
252 +
<Row style={{ alignItems: 'center', gap: 12 }}>
253 +
<View style={[styles.linkIconCircle, { backgroundColor: meta.iconBackground }]}>
254 +
<MaterialIcons name={meta.icon as any} size={22} color={meta.color} />
255 +
</View>
256 +
<Col gap={3} style={{ flex: 1 }}>
257 +
<Row style={{ alignItems: 'center', gap: 6 }}>
258 +
<Text style={styles.linkLabel}>{meta.label}</Text>
259 +
<Badge label="Consulta" variant="info" style={undefined} />
260 +
</Row>
261 +
<Text style={styles.linkTime}>
262 +
{formatMoment(new Date(ca.moment))}
263 +
</Text>
264 +
</Col>
265 +
266 +
<Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
267 +
</Row>
232 268
</Card>
233 269
</Pressable>
234 270
);
235 271
})}
236 272
</Col>
▸ 171 unchanged lines
408 444
height: "100%",
409 445
borderRadius: 99,
410 446
},
411 447
412 448
});
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,
▸ 12 unchanged lines
18 18
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
19 19
import { Section, SectionHeader } from "@/components/ui/Sections";
20 20
import { Theme, Typography, Colors, Spacing } from "@/constants/theme";
21 21
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
22 22
import { getTriggerCategory } from "@/constants/trigger-categories";
23 +
import { getCareActionMeta } from "@/constants/care-action-categories";
23 24
import {
24 25
useTrigger,
25 26
useDeleteTrigger,
26 27
} from "@/hooks/useTrigger.queries";
28 +
import { usePatchCareAction } from "@/hooks";
29 +
import { useCareActionLinkStore } from "@/stores";
27 30
import { formatMoment } from "@/lib/utils/time";
28 31
import { getMood } from "@/constants/moods";
29 32
30 33
const impactColor = (n: number) => {
31 34
if (n <= 1) return '#86efac';
▸ 5 unchanged lines
37 40
38 41
export default function TriggerDetailsScreen() {
39 42
const { id } = useLocalSearchParams<{ id: string }>();
40 43
const { data: trigger, isLoading, isError } = useTrigger(id);
41 44
const deleteTrigger = useDeleteTrigger();
45 +
const patchCareAction = usePatchCareAction();
46 +
const { pendingId, triggerLinked } = useCareActionLinkStore();
47 +
const hasPendingCareAction = !!pendingId && !triggerLinked;
42 48
const [minutesLeft, setMinutesLeft] = useState(0);
43 49
const [canDelete, setCanDelete] = useState(false);
44 50
45 51
useEffect(() => {
46 52
const updateTimer = () => {
▸ 43 unchanged lines
90 96
91 97
const meta = getTriggerCategory(trigger.category);
92 98
93 99
const handleDelete = async () => {
94 100
await deleteTrigger.mutateAsync(String(trigger.id));
95 -
96 101
router.back();
97 102
};
98 103
104 +
const handleLinkCareAction = async () => {
105 +
const careActionId = useCareActionLinkStore.getState().linkTrigger();
106 +
if (!careActionId) return;
107 +
await patchCareAction.mutateAsync({
108 +
id: String(careActionId),
109 +
data: { triggerId: trigger.id },
110 +
});
111 +
};
112 +
99 113
const deleteButtonMsg = () => {
100 114
const msg = "Excluir Registro";
101 115
const timeLeftStr = `(Disponível por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
102 116
const suffix = canDelete ? timeLeftStr : "(Expirado)";
103 117
104 118
return `${msg} ${suffix}`;
105 119
};
120 +
121 +
const hasAnyLinks = (
122 +
trigger.careActions && trigger.careActions.length > 0
123 +
) || (trigger.moodLinks && trigger.moodLinks.length > 0);
106 124
107 125
return (
108 126
<>
109 127
<Stack.Screen options={{ title: "Ver detalhes" }} />
110 128
▸ 18 unchanged lines
129 147
</Text>
130 148
</Col>
131 149
</Row>
132 150
</Card>
133 151
152 +
{/* Pending care action link banner */}
153 +
{hasPendingCareAction && (
154 +
<Pressable
155 +
onPress={handleLinkCareAction}
156 +
disabled={patchCareAction.isPending}
157 +
style={({ pressed }) => [
158 +
styles.linkBanner,
159 +
pressed && { opacity: 0.75 },
160 +
]}
161 +
>
162 +
<View style={styles.linkBannerIcon}>
163 +
<Ionicons name="link-outline" size={18} color="#1D4ED8" />
164 +
</View>
165 +
<Text style={styles.linkBannerText}>
166 +
Vincular à consulta registrada
167 +
</Text>
168 +
<Ionicons name="chevron-forward" size={16} color="#1D4ED8" />
169 +
</Pressable>
170 +
)}
171 +
134 172
{/* Linked moods */}
135 173
<Section>
136 174
<SectionHeader title="Vínculo" variant="subtle" />
137 -
{trigger.moodLinks && trigger.moodLinks.length > 0 ? (
175 +
{!!hasAnyLinks ? (
138 176
<Col gap={8}>
139 177
{trigger.moodLinks.map((link) => {
140 178
const moodDef = getMood(link.mood.selectedMood.toLowerCase());
141 179
return (
142 180
<Pressable
▸ 38 unchanged lines
181 219
</View>
182 220
</Card>
183 221
</Pressable>
184 222
);
185 223
})}
224 +
225 +
{trigger.careActions.map((ca) => {
226 +
const meta = getCareActionMeta(ca);
227 +
return (
228 +
<Pressable
229 +
key={ca.id}
230 +
onPress={() => (router.push as any)(`/care-actions/${ca.id}`)}
231 +
style={({ pressed }) => [pressed && { opacity: 0.7 }]}
232 +
>
233 +
<Card style={styles.linkCard}>
234 +
<Row style={{ alignItems: 'center', gap: 12 }}>
235 +
<View style={[styles.linkIconCircle, { backgroundColor: meta.iconBackground }]}>
236 +
<MaterialIcons name={meta.icon as any} size={22} color={meta.color} />
237 +
</View>
238 +
<Col gap={3} style={{ flex: 1 }}>
239 +
<Row style={{ alignItems: 'center', gap: 6 }}>
240 +
<Text style={styles.linkLabel}>{meta.label}</Text>
241 +
<Badge label="Consulta" variant="info" style={undefined} />
242 +
</Row>
243 +
<Text style={styles.linkTime}>
244 +
{formatMoment(new Date(ca.moment))}
245 +
</Text>
246 +
</Col>
247 +
<Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
248 +
</Row>
249 +
</Card>
250 +
</Pressable>
251 +
);
252 +
})}
186 253
</Col>
187 254
) : (
188 255
<NoLinkMessage />
189 256
)}
190 257
</Section>
▸ 87 unchanged lines
278 345
backButtonText: {
279 346
fontWeight: "700",
280 347
color: Theme.colors.text,
281 348
},
282 349
350 +
// Pending link banner
351 +
linkBanner: {
352 +
flexDirection: 'row',
353 +
alignItems: 'center',
354 +
gap: 10,
355 +
backgroundColor: '#DBEAFE',
356 +
borderRadius: 10,
357 +
padding: 12,
358 +
},
359 +
linkBannerIcon: {
360 +
width: 32,
361 +
height: 32,
362 +
borderRadius: 16,
363 +
backgroundColor: '#BFDBFE',
364 +
alignItems: 'center',
365 +
justifyContent: 'center',
366 +
},
367 +
linkBannerText: {
368 +
flex: 1,
369 +
fontSize: 14,
370 +
fontWeight: '600',
371 +
color: '#1D4ED8',
372 +
},
373 +
283 374
// Header
284 375
headerCard: {
285 376
padding: 16,
286 377
},
287 378
triggerMoment: {
▸ 30 unchanged lines
318 409
linkIconCircle: {
319 410
width: 44,
320 411
height: 44,
321 412
alignItems: "center",
322 413
justifyContent: "center",
414 +
borderRadius: 20,
323 415
},
324 416
linkMoodEmoji: {
325 417
fontSize: 28,
326 418
},
327 419
linkTime: {
▸ 14 unchanged lines
342 434
impactFill: {
343 435
height: "100%",
344 436
borderRadius: 99,
345 437
},
346 438
});