eletrotupi / tcc/ commit / fd21c7f

frontend: reorganizing triggers' metadata

Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago fd21c7f08eddd7a1198b4c46080186820aa30a6c Browse files
Parents e2b786d
Changed files
4
Additions
+277
Deletions
-44
/dev/null → frontend/app/triggers/[id].tsx
1 +
import { useState, useEffect, use } from "react";
2 +
import {
3 +
  StyleSheet,
4 +
  Text,
5 +
  View,
6 +
  ScrollView,
7 +
  Pressable,
8 +
  ActivityIndicator,
9 +
} from "react-native";
10 +
import { useLocalSearchParams, router, Stack } from "expo-router";
11 +
import { format } from "date-fns";
12 +
import { ptBR } from "date-fns/locale";
13 +

14 +
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
15 +
import { Badge } from "@/components/ui/Badge";
16 +
import { Button } from "@/components/ui/Button";
17 +
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
18 +
import { Section, SectionHeader } from "@/components/ui/Sections";
19 +
import { Theme, Typography, Colors, Spacing } from "@/constants/theme";
20 +
import { MaterialIcons } from "@expo/vector-icons";
21 +
import { getTriggerCategory } from "@/constants/trigger-categories";
22 +
import {
23 +
  useTrigger,
24 +
  useDeleteTrigger,
25 +
} from "@/hooks/useTrigger.queries";
26 +
import { formatMoment } from "@/lib/utils/time";
27 +

28 +
export default function TriggerDetailsScreen() {
29 +
  const { id } = useLocalSearchParams<{ id: string }>();
30 +
  const { data: trigger, isLoading, isError } = useTrigger(id);
31 +
  const deleteTrigger = useDeleteTrigger();
32 +
  const [minutesLeft, setMinutesLeft] = useState(0);
33 +
  const [canDelete, setCanDelete] = useState(false);
34 +

35 +
  useEffect(() => {
36 +
    const updateTimer = () => {
37 +
      const createdAt = new Date(trigger?.createdAt);
38 +
      if (!createdAt) return;
39 +

40 +
      const msSinceCreation = Date.now() - createdAt.getTime();
41 +
      const deletionWindowMs = 5 * 60 * 1000;
42 +

43 +
      const newCanDelete = msSinceCreation < deletionWindowMs;
44 +
      const newMinutesLeft = Math.max(
45 +
        0,
46 +
        Math.ceil((deletionWindowMs - msSinceCreation) / 60000),
47 +
      );
48 +

49 +
      setCanDelete(newCanDelete);
50 +
      setMinutesLeft(newMinutesLeft);
51 +
    };
52 +

53 +
    if (isLoading) return;
54 +

55 +
    updateTimer(); // Set initial values
56 +

57 +
    const interval = setInterval(updateTimer, 1000);
58 +

59 +
    return () => clearInterval(interval);
60 +
  }, [trigger, isLoading]);
61 +

62 +
  console.log("trigger", trigger)
63 +
  console.log("isLoading", isLoading)
64 +

65 +
  if (isLoading) {
66 +
    return (
67 +
      <View style={styles.centered}>
68 +
        <ActivityIndicator size="large" color={Colors.light.tint} />
69 +
      </View>
70 +
    );
71 +
  }
72 +

73 +
  if (isError || !trigger) {
74 +
    return (
75 +
      <View style={styles.centered}>
76 +
        <Text style={styles.errorText}>Registro não encontrado.</Text>
77 +
        <Pressable onPress={() => router.back()} style={styles.backButton}>
78 +
          <Text style={styles.backButtonText}>Voltar</Text>
79 +
        </Pressable>
80 +
      </View>
81 +
    );
82 +
  }
83 +

84 +
  const meta = getTriggerCategory(trigger.category);
85 +

86 +
  const handleDelete = async () => {
87 +
    await deleteTrigger.mutateAsync(String(trigger.id));
88 +

89 +
    router.back();
90 +
  };
91 +

92 +
  const deleteButtonMsg = () => {
93 +
    const msg = "Excluir Registro";
94 +
    const timeLeftStr = `(Disponível por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
95 +
    const suffix = canDelete ? timeLeftStr : "(Expirado)";
96 +

97 +
    return `${msg} ${suffix}`;
98 +
  };
99 +

100 +
  return (
101 +
    <>
102 +
      <Stack.Screen options={{ title: "Ver detalhes" }} />
103 +

104 +
      <ScrollView
105 +
        style={styles.container}
106 +
        contentContainerStyle={styles.content}
107 +
        showsVerticalScrollIndicator={false}
108 +
      >
109 +
        {/* Header card */}
110 +
        <Card style={styles.headerCard}>
111 +
          <Row style={{ alignItems: "center" }}>
112 +
            <View style={[styles.iconWrap, { backgroundColor: meta.iconBackground }]}>
113 +
              <MaterialIcons name={meta.icon} size={22} color={meta.color} />
114 +
            </View>
115 +

116 +
            <Col gap={2}>
117 +
              <Text style={styles.triggerLabel}>
118 +
                {meta.label}
119 +
              </Text>
120 +
              <Text style={styles.triggerMoment}>
121 +
                {formatMoment(new Date(trigger.moment))}
122 +
              </Text>
123 +
            </Col>
124 +
          </Row>
125 +
        </Card>
126 +

127 +
        {/* Annotation / notes */}
128 +
        {trigger.comment && (
129 +
          <Section>
130 +
            <SectionHeader title="Anotações" variant="subtle" />
131 +
            <Card style={{ padding: Spacing.cardGap }}>
132 +
              <Text style={styles.annotation}>
133 +
                "{trigger.comment?.trim()}"
134 +
              </Text>
135 +
            </Card>
136 +
          </Section>
137 +
        )}
138 +

139 +
        {/* Delete */}
140 +
        <View style={styles.section}>
141 +
          <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
142 +
          <Button
143 +
            variant="danger"
144 +
            isLoading={deleteTrigger.isPending}
145 +
            disabled={!canDelete || deleteTrigger.isPending}
146 +
            onPress={handleDelete}
147 +
            title={deleteButtonMsg()}
148 +
          />
149 +

150 +
          <SubtleInfoCard
151 +
            style={styles.deleteInfo}
152 +
            text={`
153 +
              A exclusão de registros é permitida apenas nos primeiros 5 minutos
154 +
              após o envio para garantir a integridade do histórico emocional.
155 +
              `}
156 +
          />
157 +
        </View>
158 +
      </ScrollView>
159 +
    </>
160 +
  );
161 +
}
162 +

163 +
const styles = StyleSheet.create({
164 +
  container: {
165 +
    flex: 1,
166 +
    backgroundColor: Theme.colors.background,
167 +
  },
168 +
  content: {
169 +
    padding: Spacing.containerPadding,
170 +
    gap: Spacing.sectionGap,
171 +
    paddingBottom: 48,
172 +
  },
173 +
  iconWrap: {
174 +
    width: 40,
175 +
    height: 40,
176 +
    borderRadius: 20,
177 +
    alignItems: 'center',
178 +
    justifyContent: 'center',
179 +
    marginTop: 1
180 +
  },
181 +
  headerCardContent: {
182 +
    flexDirection: "row",
183 +
    alignItems: "center",
184 +
    gap: 8,
185 +
  },
186 +
  triggerLabel: {
187 +
    ...Typography.headlineLg,
188 +
    color: Colors.light.text,
189 +
  },
190 +
  centered: {
191 +
    flex: 1,
192 +
    justifyContent: "center",
193 +
    alignItems: "center",
194 +
    gap: 12,
195 +
  },
196 +
  errorText: {
197 +
    fontSize: 15,
198 +
    color: Colors.light.textSecondary,
199 +
  },
200 +
  backButton: {
201 +
    paddingHorizontal: 20,
202 +
    paddingVertical: 10,
203 +
    backgroundColor: Theme.colors.tint,
204 +
    borderRadius: Theme.borderRadius.md,
205 +
  },
206 +
  backButtonText: {
207 +
    fontWeight: "700",
208 +
    color: Theme.colors.text,
209 +
  },
210 +

211 +
  // Header
212 +
  headerCard: {
213 +
    padding: 16,
214 +
  },
215 +
  triggerMoment: {
216 +
    fontSize: 13,
217 +
    color: Colors.light.textSecondary,
218 +
  },
219 +

220 +
  // Sections
221 +
  section: {
222 +
    gap: 8,
223 +
  },
224 +
  sectionLabel: {
225 +
    fontSize: 11,
226 +
    fontWeight: "600",
227 +
    letterSpacing: 0.8,
228 +
    color: Colors.light.textSecondary,
229 +
    textTransform: "uppercase",
230 +
  },
231 +

232 +
  // Annotation
233 +
  annotation: {
234 +
    fontSize: 14,
235 +
    lineHeight: 22,
236 +
    color: Colors.light.textSecondary,
237 +
    fontStyle: "italic",
238 +
    padding: 4,
239 +
  },
240 +
});
frontend/app/triggers/new.tsx
1 1
import {
2 2
  View,
3 3
  Text,
4 4
  StyleSheet,
5 5
  KeyboardAvoidingView,
15 15
import { Spacing, Typography, Colors, BorderRadius } from "@/constants/theme";
16 16
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
17 17
import { useCreateTrigger, useMoodEntries } from "@/hooks";
18 18
import { CategoryChips, CategoryOption } from "@/components/ui/CategoryChips";
19 19
import { TriggerCategory } from "@/constants/triggers";
20 +
import { TRIGGER_CATEGORY_DEFINITIONS } from "@/constants/trigger-categories";
20 21
import { TimePicker } from "@/components/ui/TimePicker";
21 22
import { getMood } from "@/constants/moods";
22 23
import { LinkRow } from "@/components/ui/LinkRow";
23 24

24 -
const TRIGGER_CATEGORIES: CategoryOption<TriggerCategory>[] = [
25 -
  {
26 -
    label: "Trabalho",
27 -
    value: "WORK",
28 -
    icon: <MaterialIcons size={16} color="#60A5FA" name="work" />,
29 -
  },
30 -
  {
31 -
    label: "Social",
32 -
    value: "SOCIAL",
33 -
    icon: <MaterialIcons size={16} color="#AF52DE" name="diversity-1" />,
34 -
  },
35 -
  {
36 -
    label: "Saúde",
37 -
    value: "HEALTH",
38 -
    icon: <MaterialIcons size={16} color="#34C759" name="healing" />,
39 -
  },
40 -
  {
41 -
    label: "Família",
42 -
    value: "FAMILY",
43 -
    icon: <MaterialIcons size={16} color="#FF9500" name="family-restroom" />,
44 -
  },
45 -
  {
46 -
    label: "Outros",
47 -
    value: "OTHER",
48 -
    icon: <Ionicons size={16} color="#8E8E93" name="ellipsis-horizontal" />,
49 -
  },
50 -
];
25 +
const TRIGGER_CATEGORIES: CategoryOption<TriggerCategory>[] =
26 +
  TRIGGER_CATEGORY_DEFINITIONS.map((d) => ({
27 +
    label: d.label,
28 +
    value: d.id,
29 +
    icon: <MaterialIcons size={16} color={d.color} name={d.icon} />,
30 +
  }));
51 31

52 32
export default function NewTrigger() {
53 33
  const [moment, setMoment] = useState(new Date());
54 34
  const [comment, setComment] = useState("");
55 35
  const [category, setCategory] = useState("WORK");
228 208
    alignItems: "center",
229 209
    justifyContent: "center",
230 210
    marginTop: 1,
231 211
  },
232 212
});
frontend/components/history/TriggerCard.tsx
1 1
import { Text, View, StyleSheet } from "react-native";
2 2
import { MaterialIcons } from "@expo/vector-icons";
3 3
import { Card } from "@/components/ui/Cards";
4 4
import { Badge } from "@/components/ui/Badge";
5 5
import { HistoryCard } from "@/lib/history/types";
6 6
import { Trigger } from "@/lib/api";
7 7
import { Spacing } from "@/constants/theme";
8 -

9 -
// TODO: Move these to a constant as well
10 -
const TRIGGER_META: Record<
11 -
  string,
12 -
  { label: string; variant: string; icon: keyof typeof MaterialIcons.glyphMap }
13 -
> = {
14 -
  SOCIAL: { label: "Social", variant: "blue", icon: "diversity-1" },
15 -
  FAMILY: { label: "Familiar", variant: "green", icon: "family-restroom" },
16 -
  HEALTH: { label: "Emocional", variant: "purple", icon: "healing" },
17 -
  PHYSICAL: { label: "Físico", variant: "orange", icon: "fitness-center" },
18 -
  WORK: { label: "Trabalho", variant: "blue", icon: "work" },
19 -
  OTHER: { label: "Outro", variant: "gray", icon: "help-outline" },
20 -
};
8 +
import { router } from 'expo-router';
9 +
import { getTriggerCategory } from "@/constants/trigger-categories";
21 10

22 11
type Props = { card: HistoryCard & { raw: Trigger } };
23 12

24 13
export function TriggerHistoryCard({ card }: Props) {
25 -
  const meta = TRIGGER_META[card.raw.category] ?? TRIGGER_META.OTHER;
14 +
  const meta = getTriggerCategory(card.raw.category);
26 15
  const time = new Date(card.timestamp).toLocaleTimeString("pt-BR", {
27 16
    hour: "2-digit",
28 17
    minute: "2-digit",
29 18
  });
30 19

31 20
  return (
32 -
    <Card style={styles.card}>
21 +
    <Card style={styles.card} onPress={() => router.push(`/triggers/${card.raw.id}`) }>
33 22
      <View style={styles.row}>
34 -
        <View style={[styles.iconWrap, { backgroundColor: "#fff7ed" }]}>
35 -
          <MaterialIcons name={meta.icon} size={22} color="#f97316" />
23 +
        <View style={[styles.iconWrap, { backgroundColor: meta.iconBackground }]}>
24 +
          <MaterialIcons name={meta.icon} size={22} color={meta.color} />
36 25
        </View>
37 26
        <View style={styles.body}>
38 27
          <View style={styles.titleRow}>
39 28
            <Text style={styles.title}>Gatilho Identificado</Text>
40 29
            <Text style={styles.time}>{time}</Text>
78 67
  title: { fontSize: 15, fontWeight: "600" },
79 68
  time: { fontSize: 13, opacity: 0.45 },
80 69
  summary: { fontSize: 13, opacity: 0.6, lineHeight: 18, marginBottom: 6 },
81 70
  badge: { alignSelf: "flex-start", marginTop: 4 },
82 71
});
/dev/null → frontend/constants/trigger-categories.ts
1 +
import { MaterialIcons } from "@expo/vector-icons";
2 +
import { TriggerCategory } from "@/constants/triggers";
3 +

4 +
export interface TriggerCategoryDefinition {
5 +
  id: TriggerCategory;
6 +
  label: string;
7 +
  icon: keyof typeof MaterialIcons.glyphMap;
8 +
  color: string;
9 +
  iconBackground: string;
10 +
  variant: string;
11 +
}
12 +

13 +
export const TRIGGER_CATEGORY_DEFINITIONS: TriggerCategoryDefinition[] = [
14 +
  { id: "WORK",     label: "Trabalho", icon: "work",            color: "#3b82f6", iconBackground: "#dbeafe", variant: "blue" },
15 +
  { id: "SOCIAL",   label: "Social",   icon: "diversity-1",     color: "#8b5cf6", iconBackground: "#ede9fe", variant: "purple" },
16 +
  { id: "HEALTH",   label: "Saúde",    icon: "healing",         color: "#34c272", iconBackground: "#d4f5e2", variant: "green" },
17 +
  { id: "FAMILY",   label: "Família",  icon: "family-restroom", color: "#f97316", iconBackground: "#ffedd5", variant: "orange" },
18 +
  { id: "PHYSICAL", label: "Físico",   icon: "fitness-center",  color: "#f97316", iconBackground: "#ffedd5", variant: "orange" },
19 +
  { id: "OTHER",    label: "Outro",    icon: "help-outline",    color: "#94a3b8", iconBackground: "#f1f5f9", variant: "gray" },
20 +
];
21 +

22 +
export const getTriggerCategory = (id: string): TriggerCategoryDefinition =>
23 +
  TRIGGER_CATEGORY_DEFINITIONS.find((c) => c.id === id) ??
24 +
  TRIGGER_CATEGORY_DEFINITIONS.find((c) => c.id === "OTHER")!;