eletrotupi / tcc/ commit / c121404

frontend: anchor the sleep history card on the picked day

The sleep card derived its timestamp from the creation instant, so a
record logged today for an earlier day showed up under today in the
feed. Sleep has no clock anchor by design: an estimate for a day, with
extra same-day entries read as naps, so the card now anchors on the day
the user picked, matching the API's own ordering and the insights. The
clock time, meaningless without a real anchor, is gone
Pedro Lucas Porcellis porcellis@eletrotupi.com · 9 days ago c121404b00d29de6d91827ce3a3fc0ede1a0c5ab Browse files
Parents a1a9079
Changed files
3
Additions
+27
Deletions
-19
frontend/components/history/SleepRecordCard.tsx
1 1
import { View, StyleSheet } from 'react-native'
2 2
import { Text } from '@/components/ui/Text'
3 3
import { MaterialIcons } from '@expo/vector-icons'
4 4
import { Card } from '@/components/ui/Cards'
5 5
import { Badge } from '@/components/ui/Badge'
28 28

29 29
export function SleepRecordCard({ card }: Props) {
30 30
  const badge    = sleepBadge(card.raw.average)
31 31
  const duration = formatDuration(card.raw.average)
32 32

33 -
  const time     = new Date(card.timestamp)
34 -
    .toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
35 -

36 33
  return (
37 34
    <Card style={styles.card} onPress={() => router.push(`/sleep/${card.raw.id}`)}>
38 35
      <View style={styles.row}>
39 36
        <View style={[styles.iconWrap, { backgroundColor: '#ede9fe' }]}>
40 37
          <MaterialIcons name="bedtime" size={22} color="#7c3aed" />
41 38
        </View>
42 39
        <View style={styles.body}>
43 40
          <View style={styles.titleRow}>
44 41
            <Text style={styles.title}>Sono</Text>
45 -
            <Text style={styles.time}>{time}</Text>
46 42
          </View>
47 43
          <View style={styles.durationRow}>
48 44
            <Text style={styles.duration}>{duration}</Text>
49 45
          </View>
50 46
          {card.raw.annotations ? (
86 82
    marginBottom: 4
87 83
  },
88 84
  title: {
89 85
    fontSize: 15,
90 86
    fontWeight: '600'
91 -
  },
92 -
  time: {
93 -
    fontSize: 13,
94 -
    opacity: 0.45
95 87
  },
96 88
  durationRow: {
97 89
    flexDirection: 'row',
98 90
    alignItems: 'center',
99 91
    gap: 8,
115 107
    alignSelf: 'flex-start',
116 108
    marginTop: 4,
117 109
    maxWidth: '50%'
118 110
  },
119 111
})
frontend/lib/history/mappers.ts
1 1
import type { HistoryCard } from "@/lib/history/types";
2 2
import type { MoodEntry, SleepRecord, Trigger, CareAction } from "@/lib/api";
3 3
import { getMood } from "@/constants/moods";
4 +
import { parseDateOnly } from "@/lib/utils/time";
4 5

5 6

6 7
export function mapMoodToHistoryCard(mood: MoodEntry): HistoryCard {
7 8
  const moodLabel = getMood(mood.selectedMood);
8 9
  const components = mood.moodComponents
39 40
export function mapSleepToHistoryCard(record: SleepRecord): HistoryCard {
40 41
  const totalMinutes = Math.round(record.average * 60);
41 42
  const hours = Math.floor(totalMinutes / 60);
42 43
  const minutes = totalMinutes % 60;
43 44
  const duration = `${hours}h ${minutes.toString().padStart(2, "0")}min`;
44 -
  const timestamp = typeof record.createdAt === 'string'
45 -
    ? record.createdAt
46 -
    : record.createdAt.toISOString();
47 45

48 46
  return {
49 47
    id: `sleep-${record.id}`,
50 48
    category: "sleep",
51 -
    timestamp,
49 +
    timestamp: parseDateOnly(record.date).toISOString(),
52 50
    title: "Sono",
53 51
    summary: [duration, record.annotations].filter(Boolean).join(" · "),
54 52
    badge: sleepQualityBadge(record.average),
55 53
    raw: record,
56 54
  };
160 158
        summary: "",
161 159
        raw: record,
162 160
      };
163 161
  }
164 162
}
frontend/tests/history/mappers.unit.test.ts
1 1
import {
2 2
  mapCareActionToHistoryCard,
3 3
  mapMoodToHistoryCard,
4 4
  mapSleepToHistoryCard,
5 5
  mapTriggerToHistoryCard,
11 11
  MoodEntry,
12 12
  SleepRecord,
13 13
  Trigger,
14 14
} from "@/lib/api";
15 15
import type { MoodComponent } from "@/lib/api/types";
16 +
import { parseDateOnly } from "@/lib/utils/time";
16 17

17 18
const FIXED = new Date(2025, 0, 15, 10, 0, 0);
18 19

19 20
function mood(overrides: Partial<MoodEntry> = {}): MoodEntry {
20 21
  return {
150 151
  it("maps the record into a sleep card", () => {
151 152
    const record = sleep({
152 153
      id: 4,
153 154
      average: 7.5,
154 155
      annotations: "Dormi bem",
155 -
      createdAt: new Date(2025, 0, 15, 8, 0, 0),
156 +
      date: new Date(Date.UTC(2025, 0, 15)),
157 +
      createdAt: new Date(2025, 0, 20, 8, 0, 0),
156 158
    });
157 159

158 160
    expect(mapSleepToHistoryCard(record)).toStrictEqual({
159 161
      id: "sleep-4",
160 162
      category: "sleep",
161 -
      timestamp: record.createdAt.toISOString(),
163 +
      timestamp: parseDateOnly(record.date).toISOString(),
162 164
      title: "Sono",
163 165
      summary: "7h 30min · Dormi bem",
164 166
      badge: { label: "Excelente", variant: "green" },
165 167
      raw: record,
166 168
    });
167 169
  });
168 170

169 -
  it("keeps a string timestamp as-is", () => {
170 -
    const timestamp = "2025-01-15T13:00:00.000Z";
171 -
    const card = mapSleepToHistoryCard(
172 -
      sleep({ createdAt: timestamp as unknown as Date }),
171 +
  it("anchors on the picked day, not the creation instant", () => {
172 +
    const record = sleep({
173 +
      date: new Date(Date.UTC(2025, 0, 10)),
174 +
      createdAt: new Date(2025, 0, 15, 8, 0, 0),
175 +
    });
176 +

177 +
    const card = mapSleepToHistoryCard(record);
178 +

179 +
    expect(new Date(card.timestamp).toLocaleDateString("pt-BR")).toBe(
180 +
      "10/01/2025",
173 181
    );
182 +
  });
174 183

175 -
    expect(card.timestamp).toBe(timestamp);
184 +
  it("reads the date-only string the API sends", () => {
185 +
    const record = sleep({
186 +
      date: "2025-01-10T00:00:00.000Z" as unknown as Date,
187 +
    });
188 +

189 +
    const card = mapSleepToHistoryCard(record);
190 +

191 +
    expect(new Date(card.timestamp).toLocaleDateString("pt-BR")).toBe(
192 +
      "10/01/2025",
193 +
    );
176 194
  });
177 195

178 196
  it("omits the separator when there is no annotation", () => {
179 197
    expect(mapSleepToHistoryCard(sleep({ average: 8 })).summary).toBe(
180 198
      "8h 00min",
441 459
      summary: "",
442 460
      raw: record,
443 461
    });
444 462
  });
445 463
});