feat(kanban): reporte diario al click en dia del calendario (issue 0093)
Adds a daily report dashboard accessible by clicking a day number in the
calendar view. Renders inside a full-width modal (90% width).
Backend (new file backend/reports.go):
- Type DailyReport with KPIs, rankings, done_cards list, reopened cards,
3-bucket stale list (7/14/30d), lead time avg+p50+p95, 24-hour
movement histogram, deadlines met/missed list, tag distribution and
archived count.
- DB.DailyReportFor(date, tz) uses Europe/Madrid by default; computes
[start,end) in local time, converts to UTC and queries:
* cards.completed_at in range -> done list
* card_events kind=created in range -> created counts
* card_column_history.entered_at in range -> moves + hourly
* previousColumnWasDone() -> reopened detection
* card_lock_history overlapping the day -> blocked_ms
* stale buckets: open history entries on non-done columns aged >=7d
- New route GET /api/reports/daily?date=YYYY-MM-DD&tz=Europe/Madrid.
Frontend:
- api.ts: DailyReport type + dailyReport(date, tz?) call.
- New component DailyReportView (components/DailyReport.tsx):
* 6 KPI cards (Hechas, Creadas, Movimientos, Bloqueado, Reabiertas,
Deadlines on-time %).
* 4 ranking cards (Top assignees done, Top assignees created,
Top requesters atendidas, Top requesters aportadas).
* Done cards table with click-to-jump (links open the card in board).
* Mantine BarChart with movements per hour.
* Tag chips, reopened list, deadlines list with late_ms, stale buckets.
- CalendarView wraps the day number in UnstyledButton with data-test
attribute and forwards onOpenDailyReport.
- App.handleOpenDailyReport opens modals.open size 90% with the view;
click on a card title closes the modal and jumps to the board with
highlight (reuses existing handleJumpToCard).
Tests (e2e/daily-report.spec.ts):
- Endpoint shape: kpis, done_cards, hourly_moves[24], stale buckets.
- Calendar day click opens the modal with "Reporte diario" title and
KPI labels visible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,15 +19,17 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import * as api from "../api";
|
||||
import type { Card, Metrics, User } from "../types";
|
||||
|
||||
// Hace clickable el numero del dia para abrir el reporte diario (issue 0093).
|
||||
interface Props {
|
||||
users: User[];
|
||||
cards: Card[];
|
||||
onJumpToCard?: (cardId: string) => void;
|
||||
onOpenDailyReport?: (date: string) => void;
|
||||
}
|
||||
|
||||
const DAY_LABELS = ["Lun", "Mar", "Mie", "Jue", "Vie", "Sab", "Dom"];
|
||||
|
||||
export function CalendarView({ users, cards, onJumpToCard }: Props) {
|
||||
export function CalendarView({ users, cards, onJumpToCard, onOpenDailyReport }: Props) {
|
||||
const [openDate, setOpenDate] = useState<string | null>(null);
|
||||
const [month, setMonth] = useState<Date>(new Date());
|
||||
const [assigneeId, setAssigneeId] = useState<string | null>(null);
|
||||
@@ -199,9 +201,22 @@ export function CalendarView({ users, cards, onJumpToCard }: Props) {
|
||||
}}
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<Text size="xs" fw={isToday ? 700 : 500} c={isToday ? "blue" : undefined}>
|
||||
{dayNum}
|
||||
</Text>
|
||||
<UnstyledButton
|
||||
onClick={() => cell.date && onOpenDailyReport?.(cell.date as string)}
|
||||
title="Ver reporte diario"
|
||||
style={{ alignSelf: "flex-start" }}
|
||||
data-test={`calendar-day-${cell.date}`}
|
||||
>
|
||||
<Text
|
||||
size="xs"
|
||||
fw={isToday ? 700 : 500}
|
||||
c={isToday ? "blue" : undefined}
|
||||
td={onOpenDailyReport ? "underline" : undefined}
|
||||
style={{ cursor: onOpenDailyReport ? "pointer" : "default" }}
|
||||
>
|
||||
{dayNum}
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
{stats.created > 0 && (
|
||||
<Group gap={3} wrap="nowrap">
|
||||
<IconPlus size={10} color="var(--mantine-color-blue-5)" />
|
||||
|
||||
@@ -0,0 +1,523 @@
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Badge,
|
||||
Box,
|
||||
Card as MCard,
|
||||
Divider,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
ScrollArea,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import { BarChart } from "@mantine/charts";
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconArrowBackUp,
|
||||
IconCalendarStats,
|
||||
IconCheck,
|
||||
IconClock,
|
||||
IconHourglass,
|
||||
IconLock,
|
||||
IconPlus,
|
||||
IconRefresh,
|
||||
IconTrendingUp,
|
||||
} from "@tabler/icons-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { dailyReport, type DailyReport as Report } from "../api";
|
||||
import { formatDuration } from "./format";
|
||||
import { tagColor } from "./colors";
|
||||
|
||||
interface Props {
|
||||
date: string; // YYYY-MM-DD
|
||||
onJumpToCard?: (cardId: string) => void;
|
||||
}
|
||||
|
||||
function fmtDate(s: string): string {
|
||||
try {
|
||||
const d = new Date(s + "T00:00:00");
|
||||
return d.toLocaleDateString("es-ES", {
|
||||
weekday: "long",
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
} catch {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
function KPI({
|
||||
label,
|
||||
value,
|
||||
color,
|
||||
icon,
|
||||
sub,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
color?: string;
|
||||
icon?: React.ReactNode;
|
||||
sub?: string;
|
||||
}) {
|
||||
return (
|
||||
<Paper p="sm" withBorder radius="md">
|
||||
<Group gap={6} mb={2} align="center">
|
||||
{icon}
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={600}>
|
||||
{label}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz={28} fw={700} c={color}>
|
||||
{value}
|
||||
</Text>
|
||||
{sub && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{sub}
|
||||
</Text>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function RankingList<T extends { name: string; count: number; user_id?: string }>({
|
||||
title,
|
||||
rows,
|
||||
emptyText,
|
||||
withAvatar = false,
|
||||
}: {
|
||||
title: string;
|
||||
rows: T[];
|
||||
emptyText: string;
|
||||
withAvatar?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MCard withBorder radius="md" p="sm">
|
||||
<Text fw={600} size="sm" mb={6}>
|
||||
{title}
|
||||
</Text>
|
||||
{rows.length === 0 ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
{emptyText}
|
||||
</Text>
|
||||
) : (
|
||||
<Stack gap={4}>
|
||||
{rows.map((r, i) => (
|
||||
<Group key={(r.user_id || r.name) + i} gap={6} wrap="nowrap" justify="space-between">
|
||||
<Group gap={6} wrap="nowrap" style={{ minWidth: 0, flex: 1 }}>
|
||||
{withAvatar && (
|
||||
<Avatar size={22} radius="xl" color={tagColor(r.name || String(i))}>
|
||||
{(r.name || "?").slice(0, 2).toUpperCase()}
|
||||
</Avatar>
|
||||
)}
|
||||
<Text size="sm" truncate>
|
||||
{r.name || "(sin nombre)"}
|
||||
</Text>
|
||||
</Group>
|
||||
<Badge size="sm" variant="light" color={i === 0 ? "teal" : "gray"}>
|
||||
{r.count}
|
||||
</Badge>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</MCard>
|
||||
);
|
||||
}
|
||||
|
||||
export function DailyReportView({ date, onJumpToCard }: Props) {
|
||||
const [data, setData] = useState<Report | null>(null);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setData(null);
|
||||
setErr(null);
|
||||
dailyReport(date)
|
||||
.then(setData)
|
||||
.catch((e) => setErr((e as Error).message));
|
||||
}, [date]);
|
||||
|
||||
const hourlyChartData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return data.hourly_moves.map((n, h) => ({
|
||||
hora: String(h).padStart(2, "0") + ":00",
|
||||
movimientos: n,
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
if (err) {
|
||||
return (
|
||||
<Alert color="red" icon={<IconAlertTriangle size={14} />}>
|
||||
{err}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
if (!data) {
|
||||
return (
|
||||
<Group justify="center" p="xl">
|
||||
<Loader size="sm" />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
const k = data.kpis;
|
||||
const onTimePct =
|
||||
k.deadlines_met + k.deadlines_missed > 0
|
||||
? Math.round((k.deadlines_met / (k.deadlines_met + k.deadlines_missed)) * 100)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" wrap="wrap">
|
||||
<Group gap={6}>
|
||||
<IconCalendarStats size={20} />
|
||||
<Title order={4}>Reporte diario</Title>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed" tt="capitalize">
|
||||
{fmtDate(data.date)}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 2, sm: 4, md: 6 }} spacing="xs">
|
||||
<KPI label="Hechas" value={k.done} color="teal" icon={<IconCheck size={14} color="var(--mantine-color-teal-6)" />} />
|
||||
<KPI label="Creadas" value={k.created} icon={<IconPlus size={14} />} />
|
||||
<KPI label="Movimientos" value={k.moves} icon={<IconRefresh size={14} />} />
|
||||
<KPI
|
||||
label="Bloqueado"
|
||||
value={formatDuration(k.blocked_ms)}
|
||||
color="yellow"
|
||||
icon={<IconLock size={14} color="var(--mantine-color-yellow-6)" />}
|
||||
/>
|
||||
<KPI
|
||||
label="Reabiertas"
|
||||
value={k.reopened}
|
||||
color={k.reopened > 0 ? "orange" : undefined}
|
||||
icon={<IconArrowBackUp size={14} />}
|
||||
/>
|
||||
<KPI
|
||||
label="Deadlines"
|
||||
value={onTimePct != null ? `${onTimePct}%` : "—"}
|
||||
color={onTimePct == null ? "dimmed" : onTimePct >= 80 ? "teal" : "red"}
|
||||
sub={`${k.deadlines_met} on-time / ${k.deadlines_missed} vencidos`}
|
||||
icon={<IconHourglass size={14} />}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="xs">
|
||||
<RankingList
|
||||
title="Asignado: mas hechas"
|
||||
rows={data.top_assignees_done}
|
||||
emptyText="Sin hechas con asignado."
|
||||
withAvatar
|
||||
/>
|
||||
<RankingList
|
||||
title="Asignado: mas creadas"
|
||||
rows={data.top_assignees_created}
|
||||
emptyText="Sin actor en creadas."
|
||||
withAvatar
|
||||
/>
|
||||
<RankingList
|
||||
title="Solicitante: mas atendidas"
|
||||
rows={data.top_requesters_done}
|
||||
emptyText="Sin solicitantes con hechas."
|
||||
/>
|
||||
<RankingList
|
||||
title="Solicitante: mas aportadas"
|
||||
rows={data.top_requesters_added}
|
||||
emptyText="Sin nuevas con solicitante."
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<MCard withBorder radius="md" p="sm">
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Text fw={600} size="sm">
|
||||
Tareas hechas
|
||||
</Text>
|
||||
<Group gap={6}>
|
||||
<Badge size="xs" variant="light">
|
||||
N {k.done}
|
||||
</Badge>
|
||||
<Text size="xs" c="dimmed">
|
||||
Lead time avg {data.lead_time.samples > 0 ? formatDuration(data.lead_time.avg_ms) : "—"} · p50{" "}
|
||||
{data.lead_time.samples > 0 ? formatDuration(data.lead_time.p50_ms) : "—"} · p95{" "}
|
||||
{data.lead_time.samples > 0 ? formatDuration(data.lead_time.p95_ms) : "—"}
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
{data.done_cards.length === 0 ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
Sin hechas en este dia.
|
||||
</Text>
|
||||
) : (
|
||||
<ScrollArea style={{ maxHeight: 280 }} type="auto">
|
||||
<Table verticalSpacing={4} fz="xs" highlightOnHover striped="even">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th style={{ width: 70 }}>#</Table.Th>
|
||||
<Table.Th>Titulo</Table.Th>
|
||||
<Table.Th>Solicitante</Table.Th>
|
||||
<Table.Th>Asignado</Table.Th>
|
||||
<Table.Th>Tags</Table.Th>
|
||||
<Table.Th style={{ width: 110 }}>Lead time</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data.done_cards.map((c) => (
|
||||
<Table.Tr key={c.id}>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">
|
||||
{String(c.seq_num).padStart(5, "0")}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<UnstyledButton onClick={() => onJumpToCard?.(c.id)} style={{ textAlign: "left" }}>
|
||||
<Text size="xs" fw={500} td="underline">
|
||||
{c.title}
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs">{c.requester || "—"}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs">{c.assignee_name || "—"}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap={2} wrap="wrap">
|
||||
{(c.tags || []).slice(0, 3).map((t) => (
|
||||
<Badge key={t} size="xs" variant="light" color={tagColor(t)} radius="sm">
|
||||
{t}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">
|
||||
{formatDuration(c.lead_time_ms)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</MCard>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xs">
|
||||
<MCard withBorder radius="md" p="sm">
|
||||
<Group justify="space-between" mb={6}>
|
||||
<Text fw={600} size="sm">
|
||||
Movimientos por hora
|
||||
</Text>
|
||||
<Badge size="xs" variant="light">
|
||||
{k.moves}
|
||||
</Badge>
|
||||
</Group>
|
||||
{k.moves === 0 ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
Sin movimientos.
|
||||
</Text>
|
||||
) : (
|
||||
<BarChart
|
||||
h={160}
|
||||
data={hourlyChartData}
|
||||
dataKey="hora"
|
||||
series={[{ name: "movimientos", color: "blue.6" }]}
|
||||
tickLine="y"
|
||||
withTooltip
|
||||
valueFormatter={(v: number) => String(v)}
|
||||
/>
|
||||
)}
|
||||
</MCard>
|
||||
|
||||
<MCard withBorder radius="md" p="sm">
|
||||
<Text fw={600} size="sm" mb={6}>
|
||||
Tags trabajadas
|
||||
</Text>
|
||||
{data.tags_done.length === 0 ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
Sin tags.
|
||||
</Text>
|
||||
) : (
|
||||
<Group gap={4} wrap="wrap">
|
||||
{data.tags_done.map((t) => (
|
||||
<Badge key={t.name} variant="light" color={tagColor(t.name)} size="sm">
|
||||
{t.name} · {t.count}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
</MCard>
|
||||
</SimpleGrid>
|
||||
|
||||
{data.reopened_cards.length > 0 && (
|
||||
<MCard withBorder radius="md" p="sm">
|
||||
<Group gap={6} mb={6}>
|
||||
<IconArrowBackUp size={14} color="var(--mantine-color-orange-6)" />
|
||||
<Text fw={600} size="sm">
|
||||
Reabiertas (Done → otra)
|
||||
</Text>
|
||||
<Badge size="xs" variant="light" color="orange">
|
||||
{data.reopened_cards.length}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Stack gap={4}>
|
||||
{data.reopened_cards.map((r) => (
|
||||
<Group key={r.card_id + r.ts} gap={6} wrap="nowrap" justify="space-between">
|
||||
<UnstyledButton onClick={() => onJumpToCard?.(r.card_id)} style={{ minWidth: 0, flex: 1 }}>
|
||||
<Text size="xs" truncate td="underline">
|
||||
{r.title}
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
<Text size="xs" c="dimmed">
|
||||
{r.from_column} → {r.to_column}
|
||||
</Text>
|
||||
{r.actor_name && (
|
||||
<Badge size="xs" variant="light" color="cyan">
|
||||
{r.actor_name}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</MCard>
|
||||
)}
|
||||
|
||||
{(data.deadlines.missed > 0 || data.deadlines.met > 0) && (
|
||||
<MCard withBorder radius="md" p="sm">
|
||||
<Group gap={6} mb={6}>
|
||||
<IconHourglass size={14} />
|
||||
<Text fw={600} size="sm">
|
||||
Deadlines
|
||||
</Text>
|
||||
<Badge size="xs" variant="light" color="teal">
|
||||
{data.deadlines.met} on-time
|
||||
</Badge>
|
||||
<Badge size="xs" variant="light" color="red">
|
||||
{data.deadlines.missed} vencidos
|
||||
</Badge>
|
||||
</Group>
|
||||
{data.deadlines.list.length > 0 && (
|
||||
<Stack gap={4}>
|
||||
{data.deadlines.list.map((d) => (
|
||||
<Group key={d.card_id} gap={6} justify="space-between" wrap="nowrap">
|
||||
<UnstyledButton onClick={() => onJumpToCard?.(d.card_id)} style={{ minWidth: 0, flex: 1 }}>
|
||||
<Text size="xs" truncate td="underline">
|
||||
{d.title}
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
<Text size="xs" c="red">
|
||||
+{formatDuration(d.late_ms)} tarde
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</MCard>
|
||||
)}
|
||||
|
||||
<MCard withBorder radius="md" p="sm">
|
||||
<Group gap={6} mb={6}>
|
||||
<IconTrendingUp size={14} />
|
||||
<Text fw={600} size="sm">
|
||||
Cards estancadas (al final del dia)
|
||||
</Text>
|
||||
<Badge size="xs" variant="light" color="orange">
|
||||
{data.stale_cards.d7.length}d7
|
||||
</Badge>
|
||||
<Badge size="xs" variant="light" color="red">
|
||||
{data.stale_cards.d14.length}d14
|
||||
</Badge>
|
||||
<Badge size="xs" variant="filled" color="red">
|
||||
{data.stale_cards.d30.length}d30
|
||||
</Badge>
|
||||
</Group>
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="xs">
|
||||
<Box>
|
||||
<Text size="xs" fw={500} c="orange" mb={4}>
|
||||
7-13 dias
|
||||
</Text>
|
||||
<Stack gap={2}>
|
||||
{data.stale_cards.d7.slice(0, 8).map((s) => (
|
||||
<UnstyledButton key={s.card_id} onClick={() => onJumpToCard?.(s.card_id)}>
|
||||
<Text size="xs" truncate>
|
||||
{s.title}{" "}
|
||||
<Text span c="dimmed" size="xs">
|
||||
· {s.column_name} · {s.days}d
|
||||
</Text>
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
))}
|
||||
{data.stale_cards.d7.length === 0 && (
|
||||
<Text size="xs" c="dimmed">
|
||||
Ninguna.
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text size="xs" fw={500} c="red" mb={4}>
|
||||
14-29 dias
|
||||
</Text>
|
||||
<Stack gap={2}>
|
||||
{data.stale_cards.d14.slice(0, 8).map((s) => (
|
||||
<UnstyledButton key={s.card_id} onClick={() => onJumpToCard?.(s.card_id)}>
|
||||
<Text size="xs" truncate>
|
||||
{s.title}{" "}
|
||||
<Text span c="dimmed" size="xs">
|
||||
· {s.column_name} · {s.days}d
|
||||
</Text>
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
))}
|
||||
{data.stale_cards.d14.length === 0 && (
|
||||
<Text size="xs" c="dimmed">
|
||||
Ninguna.
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text size="xs" fw={500} c="red.8" mb={4}>
|
||||
30+ dias
|
||||
</Text>
|
||||
<Stack gap={2}>
|
||||
{data.stale_cards.d30.slice(0, 8).map((s) => (
|
||||
<UnstyledButton key={s.card_id} onClick={() => onJumpToCard?.(s.card_id)}>
|
||||
<Text size="xs" truncate fw={600}>
|
||||
{s.title}{" "}
|
||||
<Text span c="dimmed" size="xs" fw={400}>
|
||||
· {s.column_name} · {s.days}d
|
||||
</Text>
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
))}
|
||||
{data.stale_cards.d30.length === 0 && (
|
||||
<Text size="xs" c="dimmed">
|
||||
Ninguna.
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</MCard>
|
||||
|
||||
<Divider />
|
||||
<Group gap={6} justify="space-between">
|
||||
<Group gap={4}>
|
||||
<IconClock size={14} />
|
||||
<Text size="xs" c="dimmed">
|
||||
TZ: {data.tz} · cards archivadas hoy: {data.archived_today}
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user