eletrotupi / tcc/ commit / 6a5c371

api/frontend: fix report timezone bug causing empty results

Date strings formatted as YYYY-MM-DD were parsed by the API as UTC
midnight, so the query range ended at T00:00:00Z and excluded all
entries logged that calendar day. Fix by sending full local-timezone ISO
timestamps from the client (startOfDay/endOfDay) and updating the API
schema to accept them with z.iso.datetime({ offset: true })

Also bump the annotation truncation limit in the report PDF
Pedro Lucas Porcellis porcellis@eletrotupi.com 29 days ago 6a5c371ce1221ee4c61aa0cb3c665e3793580dc3
Parents: 055fe40
4 file(s) changed
  • api/src/reports/ReportDocument.tsx +1 -1
  • api/src/reports/report.data.ts +2 -0
  • api/src/reports/report.router.ts +4 -2
  • frontend/app/report.tsx +3 -3
api/src/reports/ReportDocument.tsx
1 1
import React from 'react'
2 2
import {
3 3
  Document,
4 4
  Page,
5 5
  View,
▸ 498 unchanged lines
504 504
                  format(entry.moment, 'd MMM', { locale: ptBR }),
505 505
                  T.moods[entry.mood] ?? entry.mood,
506 506
                  String(entry.anxietyLevel),
507 507
                  String(entry.stressLevel),
508 508
                  String(entry.energyLevel),
509 -
                  truncate(entry.annotation, 14),
509 +
                  truncate(entry.annotation?.trim(), 73),
510 510
                ]}
511 511
                widths={[56, 78, 30, 30, 30, 75]}
512 512
                colors={[
513 513
                  undefined,
514 514
                  MOOD_COLORS[entry.mood],
▸ 12 unchanged lines
527 527
        )}
528 528
      </Page>
529 529
    </Document>
530 530
  )
531 531
}
api/src/reports/report.data.ts
1 1
import { eachDayOfInterval, format } from 'date-fns'
2 2
import { prisma } from '@app/lib/prisma'
3 3
import {
4 4
  BaseMoodOption,
5 5
  MoodComponentOption,
▸ 51 unchanged lines
57 57
    }),
58 58
  ])
59 59

60 60
  const total = moods.length
61 61

62 +
  console.log(`[report] found ${moods.length} mood entries from ${start} - ${end}`)
63 +

62 64
  const moodCounts = Object.fromEntries(
63 65
    MOOD_ORDER.map(m => [m, 0]),
64 66
  ) as Record<BaseMoodOption, number>
65 67

66 68
  for (const m of moods) moodCounts[m.selectedMood]++
▸ 65 unchanged lines
132 134
    dailyMoods,
133 135
    topComponents,
134 136
    checkinLog,
135 137
  }
136 138
}
api/src/reports/report.router.ts
1 1
import { Router, Response, NextFunction } from 'express'
2 2
import React from 'react'
3 3
import { renderToBuffer } from '@react-pdf/renderer'
4 4
import { z } from 'zod'
5 5
import { requireAuth, AuthenticatedRequest } from '@app/middleware/auth'
▸ 2 unchanged lines
8 8

9 9
const router = Router()
10 10

11 11
const ReportSchema = z
12 12
  .object({
13 -
    periodStart: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
14 -
    periodEnd: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
13 +
    periodStart: z.iso.datetime({ offset: true }),
14 +
    periodEnd: z.iso.datetime({ offset: true }),
15 15
  })
16 16
  .refine(
17 17
    (d) => {
18 18
      const start = new Date(d.periodStart)
19 19
      const end = new Date(d.periodEnd)
▸ 6 unchanged lines
26 26
router.post(
27 27
  '/',
28 28
  requireAuth,
29 29
  async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
30 30
    try {
31 +
      req.log.info("Requirements: %s", req.body.periodStart, req.body.periodEnd)
32 +

31 33
      const parsed = ReportSchema.safeParse(req.body)
32 34
      if (!parsed.success) {
33 35
        return res.status(400).json({ error: parsed.error.issues[0]?.message ?? 'Invalid request' })
34 36
      }
35 37

▸ 20 unchanged lines
56 58
    }
57 59
  },
58 60
)
59 61

60 62
export default router
frontend/app/report.tsx
1 1
import React, { useState } from 'react'
2 2
import {
3 3
  View,
4 4
  Text,
5 5
  TouchableOpacity,
6 6
  StyleSheet,
7 7
  ScrollView,
8 8
} from 'react-native'
9 9
import { Stack } from 'expo-router'
10 -
import { format, subDays } from 'date-fns'
10 +
import { format, subDays, startOfDay, endOfDay } from 'date-fns'
11 11
import { Paths, File } from 'expo-file-system'
12 12
import * as Sharing from 'expo-sharing'
13 13
import { SafeAreaView } from 'react-native-safe-area-context'
14 14
import { Button } from '@/components/ui/Button'
15 15
import { Section, SectionHeader } from '@/components/ui/Sections'
▸ 30 unchanged lines
46 46

47 47
  async function handleGenerate() {
48 48
    setLoading(true)
49 49
    try {
50 50
      const buffer = await apiClient.generateReport(
51 -
        format(startDate, 'yyyy-MM-dd'),
52 -
        format(endDate, 'yyyy-MM-dd'),
51 +
        startOfDay(startDate).toISOString(),
52 +
        endOfDay(endDate).toISOString(),
53 53
      )
54 54

55 55
      const fileName = `nexo-report-${format(startDate, 'yyyy-MM')}.pdf`
56 56
      const file = new File(Paths.cache, fileName)
57 57
      file.write(new Uint8Array(buffer))
▸ 137 unchanged lines
195 195
  },
196 196
  chipTextActive: {
197 197
    color: Colors.light.background,
198 198
  },
199 199
})