eletrotupi / tcc/ commit / 1b7bbac

frontend: display post-mood add page and link trigger

Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago 1b7bbac5b9f7913a9d8f8b0e29870cdbdbadd226 Browse files
Parents 5f371a6
Changed files
6
Additions
+599
Deletions
-6
frontend/app/(tabs)/new/_layout.tsx
1 1
import { Stack } from 'expo-router';
2 2

3 3
export const unstable_settings = {
4 4
  initialRouteName: 'index'
5 5
};
13 13
      <Stack.Screen name="index" />
14 14
      <Stack.Screen name="entry" options={{
15 15
        headerTitle: "Registrar humor",
16 16
        headerShown: true
17 17
      }} />
18 +
      <Stack.Screen name="post-mood" options={{ headerShown: false }} />
18 19
    </Stack>
19 20
  );
20 21
}
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

61 61
        component: c.id.toUpperCase(),
62 62
        intensity: intensityToValue(c.intensity),
63 63
      })),
64 64
    };
65 65

66 -
    await createMoodEntry.mutateAsync(data);
66 +
    const result = await createMoodEntry.mutateAsync(data);
67 +
    const moodId = result.mood.id;
67 68

68 69
    reset();
69 -
    router.replace("/");
70 +

71 +
    router.replace(`/new/post-mood?moodId=${moodId}`);
70 72
  };
71 73

72 74
  const editComponents = () => {
73 75
    router.push("/entry/mood-components");
74 76
  };
198 200
  },
199 201
  resumeCard: {
200 202
    padding: Spacing.cardGap,
201 203
  },
202 204
});
/dev/null → frontend/app/(tabs)/new/post-mood.tsx
1 +
import { useState } from "react";
2 +
import {
3 +
  View,
4 +
  Text,
5 +
  StyleSheet,
6 +
  TouchableOpacity,
7 +
  Pressable,
8 +
  ActivityIndicator,
9 +
} from "react-native";
10 +
import { router, useLocalSearchParams } from "expo-router";
11 +
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
12 +

13 +
import { ScreenLayout } from "@/components/ui/ScreenLayout";
14 +
import { Card } from "@/components/ui/Cards";
15 +
import { Button } from "@/components/ui/Button";
16 +
import { useAuth } from "@/context/AuthContext";
17 +
import { Colors, Spacing, Typography, BorderRadius, Shadows } from "@/constants/theme";
18 +
import { getTriggerCategory } from "@/constants/trigger-categories";
19 +
import { useTriggers, useLinkMoodToTrigger } from "@/hooks";
20 +
import { Trigger } from "@/lib/api";
21 +
import { formatMoment } from '@/lib/utils/time';
22 +

23 +
export default function PostMood() {
24 +
  const { moodId } = useLocalSearchParams<{ moodId: string }>();
25 +
  const { user } = useAuth();
26 +

27 +
  const [selectedTrigger, setSelectedTrigger] = useState<Trigger | null>(null);
28 +
  const [impact, setImpact] = useState(3);
29 +
  const [linked, setLinked] = useState(false);
30 +

31 +
  const { data: triggersData, isLoading: isLoadingTriggers } = useTriggers({ limit: 3 });
32 +
  const recentTriggers = triggersData?.entries ?? [];
33 +

34 +
  const linkMoodToTrigger = useLinkMoodToTrigger();
35 +

36 +
  const handleLink = async () => {
37 +
    if (!selectedTrigger || !moodId) return;
38 +
    await linkMoodToTrigger.mutateAsync({
39 +
      triggerId: String(selectedTrigger.id),
40 +
      moodId,
41 +
      perceivedImpact: impact,
42 +
    });
43 +
    setLinked(true);
44 +
    setSelectedTrigger(null);
45 +
  };
46 +

47 +
  const onNotificationPress = () => {
48 +
    router.push("/notifications");
49 +
  };
50 +

51 +
  return (
52 +
    <ScreenLayout
53 +
      userName={user?.firstName}
54 +
      userAvatar={user?.avatarURL}
55 +
      onNotificationPress={onNotificationPress}
56 +
    >
57 +
      {/* Confirmation block */}
58 +
      <View style={styles.confirmationBlock}>
59 +
        <View style={styles.outerRing}>
60 +
          <View style={styles.innerCircle}>
61 +
            <Ionicons name="checkmark" size={32} color="#fff" />
62 +
          </View>
63 +
        </View>
64 +
        <Text style={styles.confirmTitle}>Humor Salvo!</Text>
65 +
        <Text style={styles.confirmSubtitle}>
66 +
          Seu registro foi armazenado com sucesso.
67 +
        </Text>
68 +
      </View>
69 +

70 +
      {/* Link prompt card */}
71 +
      {linked ? (
72 +
        <View style={styles.linkedRow}>
73 +
          <Ionicons name="checkmark-circle" size={20} color={Colors.light.tint} />
74 +
          <Text style={styles.linkedText}>Gatilho vinculado</Text>
75 +
        </View>
76 +
      ) : (
77 +
        <Card style={styles.linkCard}>
78 +
          {selectedTrigger ? (
79 +
            /* Impact picker state */
80 +
            <View style={styles.impactPicker}>
81 +
              {/* Selected trigger header */}
82 +
              <View style={styles.selectedTriggerRow}>
83 +
                {(() => {
84 +
                  const cat = getTriggerCategory(selectedTrigger.category);
85 +
                  return (
86 +
                    <>
87 +
                      <View style={[styles.triggerIconBubble, { backgroundColor: cat.iconBackground }]}>
88 +
                        <MaterialIcons name={cat.icon} size={18} color={cat.color} />
89 +
                      </View>
90 +
                      <Text style={styles.selectedTriggerLabel} numberOfLines={1}>
91 +
                        {cat.label}
92 +
                      </Text>
93 +
                    </>
94 +
                  );
95 +
                })()}
96 +
                <TouchableOpacity
97 +
                  onPress={() => setSelectedTrigger(null)}
98 +
                  hitSlop={8}
99 +
                  style={styles.deselectButton}
100 +
                >
101 +
                  <Ionicons name="close" size={18} color={Colors.light.textSecondary} />
102 +
                </TouchableOpacity>
103 +
              </View>
104 +

105 +
              <Text style={styles.impactLabel}>Qual foi o impacto?</Text>
106 +

107 +
              <View style={styles.impactButtons}>
108 +
                {[1, 2, 3, 4, 5].map((n) => {
109 +
                  const isSelected = impact === n;
110 +
                  return (
111 +
                    <TouchableOpacity
112 +
                      key={n}
113 +
                      style={[
114 +
                        styles.impactCircle,
115 +
                        isSelected && styles.impactCircleSelected,
116 +
                      ]}
117 +
                      onPress={() => setImpact(n)}
118 +
                      activeOpacity={0.7}
119 +
                    >
120 +
                      <Text
121 +
                        style={[
122 +
                          styles.impactCircleText,
123 +
                          isSelected && styles.impactCircleTextSelected,
124 +
                        ]}
125 +
                      >
126 +
                        {n}
127 +
                      </Text>
128 +
                    </TouchableOpacity>
129 +
                  );
130 +
                })}
131 +
              </View>
132 +

133 +
              <Button
134 +
                title="Vincular"
135 +
                onPress={handleLink}
136 +
                loading={linkMoodToTrigger.isPending}
137 +
                style={{ marginTop: 4, ...Shadows.lg }}
138 +
              />
139 +
            </View>
140 +
          ) : (
141 +
            /* Trigger selection state */
142 +
            <>
143 +
              <Text style={styles.linkCardTitle}>
144 +
                Algo aconteceu que pode ter influenciado esse humor?
145 +
              </Text>
146 +
              <Text style={styles.linkCardSubtitle}>
147 +
                Selecione um gatilho recente ou adicione um novo.
148 +
              </Text>
149 +

150 +
              {isLoadingTriggers ? (
151 +
                <ActivityIndicator style={{ marginTop: 12 }} />
152 +
              ) : (
153 +
                <View style={styles.triggerGrid}>
154 +
                  {recentTriggers.map((trigger) => {
155 +
                    const cat = getTriggerCategory(trigger.category);
156 +
                    return (
157 +
                      <Pressable
158 +
                        key={trigger.id}
159 +
                        style={({ pressed }) => [
160 +
                          styles.triggerGridItem,
161 +
                          pressed && { opacity: 0.7 },
162 +
                        ]}
163 +
                        onPress={() => setSelectedTrigger(trigger)}
164 +
                      >
165 +
                        <View style={[styles.triggerIconBubble, { backgroundColor: cat.iconBackground }]}>
166 +
                          <MaterialIcons name={cat.icon} size={20} color={cat.color} />
167 +
                        </View>
168 +
                        <Text style={styles.triggerItemLabel} numberOfLines={2}>
169 +
                          {cat.label}
170 +
                        </Text>
171 +
                        <Text style={styles.triggerItemLabel} numberOfLines={2}>
172 +
                          {formatMoment(trigger.moment)}
173 +
                        </Text>
174 +
                      </Pressable>
175 +
                    );
176 +
                  })}
177 +

178 +
                  {/* New trigger item */}
179 +
                  <Pressable
180 +
                    style={({ pressed }) => [
181 +
                      styles.triggerGridItem,
182 +
                      styles.newTriggerItem,
183 +
                      pressed && { opacity: 0.7 },
184 +
                    ]}
185 +
                    onPress={() => router.push(`/triggers/new?moodId=${moodId}`)}
186 +
                  >
187 +
                    <Ionicons
188 +
                      name="add-circle-outline"
189 +
                      size={24}
190 +
                      color={Colors.light.textSecondary}
191 +
                    />
192 +
                    <Text style={styles.newTriggerLabel}>Novo gatilho</Text>
193 +
                  </Pressable>
194 +
                </View>
195 +
              )}
196 +
            </>
197 +
          )}
198 +
        </Card>
199 +
      )}
200 +
    </ScreenLayout>
201 +
  );
202 +
}
203 +

204 +
const styles = StyleSheet.create({
205 +
  confirmationBlock: {
206 +
    alignItems: "center",
207 +
    marginBottom: Spacing.sectionGap,
208 +
    gap: 12,
209 +
  },
210 +
  outerRing: {
211 +
    width: 88,
212 +
    height: 88,
213 +
    borderRadius: 44,
214 +
    backgroundColor: "#E7FDEF",
215 +
    alignItems: "center",
216 +
    justifyContent: "center",
217 +
  },
218 +
  innerCircle: {
219 +
    width: 64,
220 +
    height: 64,
221 +
    borderRadius: 32,
222 +
    backgroundColor: Colors.light.tint,
223 +
    alignItems: "center",
224 +
    justifyContent: "center",
225 +
  },
226 +
  confirmTitle: {
227 +
    ...Typography.headlineMd,
228 +
    fontWeight: "700",
229 +
    color: Colors.light.text,
230 +
    textAlign: "center",
231 +
  },
232 +
  confirmSubtitle: {
233 +
    ...Typography.bodyMd,
234 +
    color: Colors.light.textSecondary,
235 +
    textAlign: "center",
236 +
  },
237 +
  linkCard: {
238 +
    padding: Spacing.cardGap,
239 +
    marginBottom: Spacing.sectionGap,
240 +
  },
241 +
  linkCardTitle: {
242 +
    ...Typography.bodyMd,
243 +
    fontWeight: "700",
244 +
    color: Colors.light.text,
245 +
    marginBottom: 4,
246 +
  },
247 +
  linkCardSubtitle: {
248 +
    ...Typography.bodyMd,
249 +
    color: Colors.light.textSecondary,
250 +
    marginBottom: 16,
251 +
  },
252 +
  triggerGrid: {
253 +
    flexDirection: "row",
254 +
    flexWrap: "wrap",
255 +
    gap: 10,
256 +
  },
257 +
  triggerGridItem: {
258 +
    width: "48%",
259 +
    minHeight: 80,
260 +
    backgroundColor: "#fff",
261 +
    borderRadius: BorderRadius.md,
262 +
    borderWidth: 1,
263 +
    borderColor: Colors.light.divider,
264 +
    alignItems: "center",
265 +
    justifyContent: "center",
266 +
    padding: 10,
267 +
    gap: 6,
268 +
    ...Shadows.sm,
269 +
  },
270 +
  newTriggerItem: {
271 +
    borderStyle: "dashed",
272 +
    borderColor: Colors.light.borderDashed,
273 +
    backgroundColor: "transparent",
274 +
    shadowColor: "#000000",
275 +
    shadowOffset: { width: 0, height: 0 },
276 +
    shadowOpacity: 0,
277 +
    shadowRadius: 0,
278 +
    elevation: 0,
279 +
  },
280 +
  triggerIconBubble: {
281 +
    width: 36,
282 +
    height: 36,
283 +
    borderRadius: 18,
284 +
    alignItems: "center",
285 +
    justifyContent: "center",
286 +
  },
287 +
  triggerItemLabel: {
288 +
    ...Typography.labelSm,
289 +
    color: Colors.light.text,
290 +
    textAlign: "center",
291 +
  },
292 +
  newTriggerLabel: {
293 +
    ...Typography.labelSm,
294 +
    color: Colors.light.textSecondary,
295 +
    textAlign: "center",
296 +
  },
297 +
  // Impact picker
298 +
  impactPicker: {
299 +
    gap: 12,
300 +
  },
301 +
  selectedTriggerRow: {
302 +
    flexDirection: "row",
303 +
    alignItems: "center",
304 +
    gap: 10,
305 +
    marginBottom: 4,
306 +
  },
307 +
  selectedTriggerLabel: {
308 +
    ...Typography.bodyMd,
309 +
    fontWeight: "600",
310 +
    color: Colors.light.text,
311 +
    flex: 1,
312 +
  },
313 +
  deselectButton: {
314 +
    padding: 4,
315 +
  },
316 +
  impactLabel: {
317 +
    ...Typography.bodyMd,
318 +
    fontWeight: "600",
319 +
    color: Colors.light.text,
320 +
  },
321 +
  impactButtons: {
322 +
    flexDirection: "row",
323 +
    gap: 10,
324 +
    justifyContent: "center",
325 +
  },
326 +
  impactCircle: {
327 +
    width: 48,
328 +
    height: 48,
329 +
    borderRadius: 24,
330 +
    backgroundColor: "#fff",
331 +
    borderWidth: 1.5,
332 +
    borderColor: Colors.light.divider,
333 +
    alignItems: "center",
334 +
    justifyContent: "center",
335 +
  },
336 +
  impactCircleSelected: {
337 +
    backgroundColor: Colors.light.tint + "1A",
338 +
    borderColor: Colors.light.tint,
339 +
  },
340 +
  impactCircleText: {
341 +
    fontSize: 16,
342 +
    fontWeight: "600",
343 +
    color: Colors.light.text,
344 +
  },
345 +
  impactCircleTextSelected: {
346 +
    color: Colors.light.text,
347 +
  },
348 +
  // Linked success
349 +
  linkedRow: {
350 +
    flexDirection: "row",
351 +
    alignItems: "center",
352 +
    gap: 8,
353 +
    marginBottom: Spacing.sectionGap,
354 +
    paddingHorizontal: 4,
355 +
  },
356 +
  linkedText: {
357 +
    ...Typography.bodyMd,
358 +
    fontWeight: "600",
359 +
    color: Colors.light.tint,
360 +
  },
361 +
  moodList: {
362 +
    gap: 8,
363 +
  },
364 +
});
/dev/null → frontend/app/entry/post-mood-links.tsx
1 +
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, ActivityIndicator } from 'react-native';
2 +
import { useState } from 'react';
3 +
import { router, useLocalSearchParams } from 'expo-router';
4 +
import { Ionicons, MaterialIcons } from '@expo/vector-icons';
5 +
import { Button } from '@/components/ui/Button';
6 +
import { Section, SectionHeader } from '@/components/ui/Sections';
7 +
import { Spacing, Typography, Colors, BorderRadius } from '@/constants/theme';
8 +
import { useTriggers, useLinkMoodToTrigger } from '@/hooks';
9 +
import { getTriggerCategory } from '@/constants/trigger-categories';
10 +
import { Trigger } from '@/lib/api';
11 +

12 +
type FlowState = 'PICK_TRIGGER' | 'PICK_IMPACT';
13 +

14 +
export default function PostMoodLinks() {
15 +
  const { moodId } = useLocalSearchParams<{ moodId: string }>();
16 +
  const [flowState, setFlowState] = useState<FlowState>('PICK_TRIGGER');
17 +
  const [selectedTrigger, setSelectedTrigger] = useState<Trigger | null>(null);
18 +
  const [impact, setImpact] = useState<number>(3);
19 +

20 +
  const { data, isLoading } = useTriggers({ limit: 5 });
21 +
  const linkMood = useLinkMoodToTrigger();
22 +

23 +
  const triggers = data?.entries ?? [];
24 +

25 +
  const handleSelectTrigger = (trigger: Trigger) => {
26 +
    setSelectedTrigger(trigger);
27 +
    setFlowState('PICK_IMPACT');
28 +
  };
29 +

30 +
  const handleSaveLink = async () => {
31 +
    if (!selectedTrigger || !moodId) return;
32 +
    await linkMood.mutateAsync({
33 +
      triggerId: String(selectedTrigger.id),
34 +
      moodId,
35 +
      perceivedImpact: impact,
36 +
    });
37 +
    router.back();
38 +
  };
39 +

40 +
  if (flowState === 'PICK_TRIGGER') {
41 +
    return (
42 +
      <View style={styles.container}>
43 +
        <View style={styles.header}>
44 +
          <Text style={styles.title}>Algo influenciou esse humor?</Text>
45 +
          <Text style={styles.subtitle}>
46 +
            Selecione um gatilho recente para vincular
47 +
          </Text>
48 +
        </View>
49 +

50 +
        <ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
51 +
          {isLoading && <ActivityIndicator style={{ marginTop: 20 }} />}
52 +

53 +
          {!isLoading && triggers.length === 0 && (
54 +
            <Text style={styles.emptyText}>Nenhum gatilho recente</Text>
55 +
          )}
56 +

57 +
          {triggers.map((trigger) => {
58 +
            const cat = getTriggerCategory(trigger.category);
59 +
            return (
60 +
              <TouchableOpacity
61 +
                key={trigger.id}
62 +
                style={styles.triggerCard}
63 +
                activeOpacity={0.7}
64 +
                onPress={() => handleSelectTrigger(trigger)}
65 +
              >
66 +
                <View style={[styles.triggerIcon, { backgroundColor: cat.iconBackground }]}>
67 +
                  <MaterialIcons name={cat.icon} size={18} color={cat.color} />
68 +
                </View>
69 +
                <View style={styles.triggerText}>
70 +
                  <Text style={styles.triggerCategory}>{cat.label}</Text>
71 +
                  {trigger.comment ? (
72 +
                    <Text style={styles.triggerComment} numberOfLines={1}>{trigger.comment}</Text>
73 +
                  ) : null}
74 +
                </View>
75 +
                <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
76 +
              </TouchableOpacity>
77 +
            );
78 +
          })}
79 +

80 +
          <Button title="Pular" variant="ghost" onPress={() => router.back()} />
81 +
        </ScrollView>
82 +
      </View>
83 +
    );
84 +
  }
85 +

86 +
  // PICK_IMPACT
87 +
  const cat = selectedTrigger ? getTriggerCategory(selectedTrigger.category) : null;
88 +
  return (
89 +
    <View style={styles.container}>
90 +
      <View style={styles.header}>
91 +
        <Text style={styles.title}>Qual foi o impacto?</Text>
92 +
        <Text style={styles.subtitle}>De 1 (leve) a 5 (muito forte)</Text>
93 +
      </View>
94 +

95 +
      {selectedTrigger && cat && (
96 +
        <View style={styles.selectedTrigger}>
97 +
          <View style={[styles.triggerIcon, { backgroundColor: cat.iconBackground }]}>
98 +
            <MaterialIcons name={cat.icon} size={18} color={cat.color} />
99 +
          </View>
100 +
          <Text style={styles.selectedTriggerLabel}>{cat.label}</Text>
101 +
        </View>
102 +
      )}
103 +

104 +
      <View style={styles.impactRow}>
105 +
        {[1, 2, 3, 4, 5].map((v) => (
106 +
          <TouchableOpacity
107 +
            key={v}
108 +
            style={[styles.impactDot, impact === v && styles.impactDotActive]}
109 +
            activeOpacity={0.7}
110 +
            onPress={() => setImpact(v)}
111 +
          >
112 +
            <Text style={[styles.impactLabel, impact === v && styles.impactLabelActive]}>{v}</Text>
113 +
          </TouchableOpacity>
114 +
        ))}
115 +
      </View>
116 +

117 +
      <Button
118 +
        title="Vincular"
119 +
        onPress={handleSaveLink}
120 +
        loading={linkMood.isPending}
121 +
        style={{ marginTop: 8 }}
122 +
      />
123 +
      <Button
124 +
        title="Voltar"
125 +
        variant="ghost"
126 +
        onPress={() => setFlowState('PICK_TRIGGER')}
127 +
      />
128 +
    </View>
129 +
  );
130 +
}
131 +

132 +
const styles = StyleSheet.create({
133 +
  container: {
134 +
    flex: 1,
135 +
    paddingHorizontal: Spacing.containerPadding,
136 +
    paddingTop: 24,
137 +
    paddingBottom: 36,
138 +
    gap: 16,
139 +
  },
140 +
  header: { gap: 6 },
141 +
  title: { ...Typography.headlineMd },
142 +
  subtitle: { ...Typography.bodyMd, color: Colors.light.textSecondary },
143 +
  scrollContent: { gap: 10, paddingBottom: 20 },
144 +
  emptyText: { ...Typography.bodyMd, color: Colors.light.textSecondary, textAlign: 'center', marginTop: 20 },
145 +
  triggerCard: {
146 +
    flexDirection: 'row',
147 +
    alignItems: 'center',
148 +
    backgroundColor: '#fff',
149 +
    borderRadius: BorderRadius.lg,
150 +
    padding: Spacing.cardGap,
151 +
    borderWidth: 1,
152 +
    borderColor: Colors.light.divider,
153 +
    gap: 12,
154 +
  },
155 +
  triggerIcon: {
156 +
    width: 36,
157 +
    height: 36,
158 +
    borderRadius: BorderRadius.md,
159 +
    alignItems: 'center',
160 +
    justifyContent: 'center',
161 +
  },
162 +
  triggerText: { flex: 1, gap: 2 },
163 +
  triggerCategory: { ...Typography.bodyMd, fontWeight: '600' },
164 +
  triggerComment: { ...Typography.labelSm, color: Colors.light.textSecondary },
165 +
  selectedTrigger: {
166 +
    flexDirection: 'row',
167 +
    alignItems: 'center',
168 +
    gap: 12,
169 +
    padding: Spacing.cardGap,
170 +
    backgroundColor: Colors.light.surface,
171 +
    borderRadius: BorderRadius.lg,
172 +
    borderWidth: 1,
173 +
    borderColor: Colors.light.divider,
174 +
  },
175 +
  selectedTriggerLabel: { ...Typography.bodyMd, fontWeight: '600' },
176 +
  impactRow: {
177 +
    flexDirection: 'row',
178 +
    justifyContent: 'center',
179 +
    gap: 12,
180 +
    marginVertical: 8,
181 +
  },
182 +
  impactDot: {
183 +
    width: 48,
184 +
    height: 48,
185 +
    borderRadius: 24,
186 +
    borderWidth: 2,
187 +
    borderColor: Colors.light.divider,
188 +
    alignItems: 'center',
189 +
    justifyContent: 'center',
190 +
    backgroundColor: '#fff',
191 +
  },
192 +
  impactDotActive: {
193 +
    borderColor: Colors.light.tint,
194 +
    backgroundColor: Colors.light.tint + '1A',
195 +
  },
196 +
  impactLabel: { fontSize: 18, fontWeight: '600', color: Colors.light.textSecondary },
197 +
  impactLabelActive: { color: Colors.light.text },
198 +
});
frontend/app/triggers/new.tsx
1 1
import {
2 2
  View,
3 3
  Text,
4 4
  StyleSheet,
5 5
  KeyboardAvoidingView,
12 12
import { TextArea } from "@/components/ui/Input";
13 13
import { Button } from "@/components/ui/Button";
14 14
import { Section, SectionHeader } from "@/components/ui/Sections";
15 15
import { Spacing, Typography, Colors, BorderRadius } from "@/constants/theme";
16 16
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
17 -
import { useCreateTrigger, useMoodEntries } from "@/hooks";
17 +
import { useCreateTrigger, useMoodEntries, useLinkMoodToTrigger } from "@/hooks";
18 18
import { CategoryChips, CategoryOption } from "@/components/ui/CategoryChips";
19 19
import { TriggerCategory } from "@/constants/triggers";
20 20
import { TRIGGER_CATEGORY_DEFINITIONS } from "@/constants/trigger-categories";
21 21
import { TimePicker } from "@/components/ui/TimePicker";
22 22
import { getMood } from "@/constants/moods";
30 30
  }));
31 31

32 32
export default function NewTrigger() {
33 33
  const [moment, setMoment] = useState(new Date());
34 34
  const [comment, setComment] = useState("");
35 -
  const { category: initialCategory } = useLocalSearchParams<{ category?: string }>();
35 +
  const { category: initialCategory, moodId: linkedMoodId } = useLocalSearchParams<{ category?: string; moodId?: string }>();
36 36
  const [category, setCategory] = useState(initialCategory ?? "WORK");
37 37
  const createTrigger = useCreateTrigger();
38 +
  const linkMoodToTrigger = useLinkMoodToTrigger();
38 39
  const [linkMood, setLinkMood] = useState(false);
39 40
  const { data: moodEntries, isLoading: isLoadingMood } = useMoodEntries({
40 41
    limit: 1,
41 42
  });
42 43

51 52
      comment,
52 53
      category,
53 54
      ...(linkMood && latestMood ? { moodId: latestMood.id } : null),
54 55
    };
55 56

56 -
    await createTrigger.mutateAsync(data);
57 -
    router.replace("/(tabs)/actions");
57 +
    const result = await createTrigger.mutateAsync(data);
58 +

59 +
    if (linkedMoodId) {
60 +
      await linkMoodToTrigger.mutateAsync({
61 +
        triggerId: String(result.trigger.id),
62 +
        moodId: linkedMoodId,
63 +
        perceivedImpact: 3,
64 +
      });
65 +
      router.back();
66 +
    } else {
67 +
      router.replace("/(tabs)/actions");
68 +
    }
58 69
  };
59 70

60 71
  return (
61 72
    <KeyboardAvoidingView behavior="height" style={styles.keyboardView}>
62 73
      <ScrollView scrollEventThrottle={16}>
209 220
    alignItems: "center",
210 221
    justifyContent: "center",
211 222
    marginTop: 1,
212 223
  },
213 224
});
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 5

131 131
      }, 2000);
132 132
    },
133 133
  });
134 134
};
135 135

136 +
export const useLinkMoodToTrigger = () => {
137 +
  const queryClient = useQueryClient();
138 +

139 +
  return useMutation({
140 +
    mutationFn: ({ triggerId, moodId, perceivedImpact }: {
141 +
      triggerId: string;
142 +
      moodId: string;
143 +
      perceivedImpact: number
144 +
    }) =>
145 +
      apiClient.linkMoodToTrigger(triggerId, { moodId: Number(moodId), perceivedImpact }),
146 +

147 +
    onSuccess: (_data, { triggerId }) => {
148 +
      queryClient.invalidateQueries({ queryKey: triggerKeys.detail(triggerId) });
149 +
    },
150 +
  });
151 +
};
152 +

136 153
// Delete a mood entry with optimistic removal
137 154
export const useDeleteTrigger = () => {
138 155
  const queryClient = useQueryClient();
139 156

140 157
  return useMutation({
163 180
        queryClient.invalidateQueries({ queryKey: insightKeys.byType("TRIGGER_PATTERN") });
164 181
      }, 2000);
165 182
    },
166 183
  });
167 184
};