diff --git a/e2e/mobile-navigation.spec.ts b/e2e/mobile-navigation.spec.ts index 5b1c9ff..6d90812 100644 --- a/e2e/mobile-navigation.spec.ts +++ b/e2e/mobile-navigation.spec.ts @@ -12,35 +12,36 @@ async function openDashboard(page: Page, width: number) { } async function expectSummaryCardsFit(page: Page, contentWidth: number) { - for (const card of [ - page.getByTestId('summary-card-today'), - page.getByTestId('summary-card-week'), - page.getByTestId('summary-card-month'), - ]) { - await expect(card).toBeVisible() - const metrics = await card.evaluate((element) => { - const style = getComputedStyle(element) - return { - clientHeight: element.clientHeight, - clientWidth: element.clientWidth, - gap: Number.parseFloat(style.rowGap), - paddingBottom: Number.parseFloat(style.paddingBottom), - paddingTop: Number.parseFloat(style.paddingTop), - scrollHeight: element.scrollHeight, - scrollWidth: element.scrollWidth, - width: element.getBoundingClientRect().width, - } - }) + const today = page.getByTestId('summary-card-today') + const week = page.getByTestId('summary-card-week') + const month = page.getByTestId('summary-card-month') - // Com os dados normais da conta de teste, os três resumos devem continuar - // intrínsecos — ocupar toda a coluna reintroduziria a área vazia reportada. - expect(metrics.width).toBeLessThan(contentWidth) + for (const card of [today, week, month]) { + await expect(card).toBeVisible() + const metrics = await card.evaluate((element) => ({ + clientHeight: element.clientHeight, + clientWidth: element.clientWidth, + scrollHeight: element.scrollHeight, + scrollWidth: element.scrollWidth, + })) + // Nenhum card pode ter overflow interno, nem com saldos longos. expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth) expect(metrics.scrollHeight).toBeLessThanOrEqual(metrics.clientHeight) - expect(metrics.gap).toBe(0) - expect(metrics.paddingTop).toBe(0) - expect(metrics.paddingBottom).toBe(0) } + + const [todayBox, weekBox, monthBox] = await Promise.all([ + today.boundingBox(), + week.boundingBox(), + month.boundingBox(), + ]) + if (!todayBox || !weekBox || !monthBox) throw new Error('summary cards sem boundingBox') + + // "Hoje" em destaque ocupa (quase) toda a largura da coluna — sem a área vazia à direita. + expect(todayBox.width).toBeGreaterThan(contentWidth * 0.9) + // Semana e Mês ficam lado a lado (mesma linha) e cada um cabe na metade da coluna. + expect(Math.abs(weekBox.y - monthBox.y)).toBeLessThanOrEqual(2) + expect(weekBox.width).toBeLessThanOrEqual(contentWidth / 2 + 2) + expect(monthBox.width).toBeLessThanOrEqual(contentWidth / 2 + 2) } async function stubExtremeSummary(page: Page) { diff --git a/src/components/daily-summary.tsx b/src/components/daily-summary.tsx index 20f693f..47454d7 100644 --- a/src/components/daily-summary.tsx +++ b/src/components/daily-summary.tsx @@ -1,72 +1,154 @@ 'use client' import { m } from 'motion/react' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Card } from '@/components/ui/card' import { ActivityTag } from '@/components/activity-selector' import { formatBRT, formatMinutes } from '@/lib/dates' +import { cn } from '@/lib/utils' import type { BalanceSummary, DailySummary } from '@/types' interface DailySummaryProps { summary: DailySummary } -function BalanceCard({ +const REVEAL_EASE = [0.16, 1, 0.3, 1] as const + +/** Saldo negativo (débito) em vermelho, positivo (crédito) em verde, zero neutro. */ +function saldoClass(minutes: number): string { + if (minutes < 0) return 'text-destructive' + if (minutes > 0) return 'text-emerald-600 dark:text-emerald-400' + return 'text-foreground' +} + +function ProgressRing({ actual, expected }: { actual: number; expected: number }) { + const radius = 24 + const circumference = 2 * Math.PI * radius + const ratio = expected > 0 ? Math.min(actual / expected, 1) : 0 + const label = expected > 0 ? `${Math.round((actual / expected) * 100)}%` : '—' + + return ( + // Decorativo: os números (feito, previsto, saldo) já estão no texto ao lado. + + ) +} + +/** "Hoje" em destaque: número grande + anel de progresso (feito/previsto). */ +function TodayCard({ balance }: { balance: BalanceSummary }) { + return ( + +
+

Hoje

+

+ {formatMinutes(balance.actualMinutes)} +

+

+ de {formatMinutes(balance.expectedMinutes)} · saldo{' '} + + {formatMinutes(balance.balanceMinutes)} + +

+
+ +
+ ) +} + +/** Semana / Mês: cards compactos lado a lado. */ +function CompactCard({ title, balance, - cumulativeBalance, + cumulative, testId, }: { title: string balance: BalanceSummary - cumulativeBalance?: number + cumulative?: number testId: string }) { return ( - - - {title} - - -

{formatMinutes(balance.actualMinutes)}

-
-

Previsto: {formatMinutes(balance.expectedMinutes)}

-

Saldo: {formatMinutes(balance.balanceMinutes)}

- {cumulativeBalance != null &&

Acumulado: {formatMinutes(cumulativeBalance)}

} -
-
+ +

{title}

+

+ {formatMinutes(balance.actualMinutes)} +

+

+ saldo + + {formatMinutes(balance.balanceMinutes)} + +

+ {cumulative != null && ( +

+ acum. {formatMinutes(cumulative)} +

+ )}
) } export function DailySummaryCard({ summary }: DailySummaryProps) { + const cumulative = summary.month.showCumulativeBalance + ? summary.month.cumulativeBalance ?? undefined + : undefined + return (
-
- {[ - { title: 'Hoje', testId: 'summary-card-today', balance: summary.today, cumulative: undefined as number | undefined }, - { title: 'Semana', testId: 'summary-card-week', balance: summary.week, cumulative: undefined as number | undefined }, - { - title: 'Mês', - testId: 'summary-card-month', - balance: summary.month, - cumulative: summary.month.showCumulativeBalance - ? summary.month.cumulativeBalance ?? undefined - : undefined as number | undefined, - }, - ].map(({ title, testId, balance, cumulative }, i) => ( - - - - ))} +
+ + + + + + + + +
{summary.entries.length > 0 && (