eletrotupi / tcc/ commit / bdaa191

frontend: display any links for triggers or mood entries

Pedro Lucas Porcellis porcellis@eletrotupi.com 1 month ago bdaa191df0c0dac8feaaf032ece8a7f4046f9976
Parents: fa18535
2 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
});