chore: auto-commit (28 archivos)
- app.md - auth.go - chat.go - chat.log - db.go - frontend/package.json - frontend/pnpm-lock.yaml - frontend/src/App.tsx - frontend/src/Root.tsx - frontend/src/api.ts - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import {
|
||||
Box,
|
||||
Center,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { MonthPickerInput } from "@mantine/dates";
|
||||
import { IconCheckbox, IconPlus } from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import * as api from "../api";
|
||||
import type { Metrics, User } from "../types";
|
||||
|
||||
interface Props {
|
||||
users: User[];
|
||||
}
|
||||
|
||||
const DAY_LABELS = ["Lun", "Mar", "Mie", "Jue", "Vie", "Sab", "Dom"];
|
||||
|
||||
export function CalendarView({ users }: Props) {
|
||||
const [month, setMonth] = useState<Date>(new Date());
|
||||
const [assigneeId, setAssigneeId] = useState<string | null>(null);
|
||||
const [data, setData] = useState<Metrics | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
const start = dayjs(month).startOf("month").format("YYYY-MM-DD");
|
||||
const end = dayjs(month).endOf("month").format("YYYY-MM-DD");
|
||||
api
|
||||
.getMetrics({ from: start, to: end, assignee_id: assigneeId || undefined })
|
||||
.then((m) => {
|
||||
if (!cancelled) setData(m);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [month, assigneeId]);
|
||||
|
||||
const userOptions = useMemo(
|
||||
() => users.map((u) => ({ value: u.id, label: u.display_name || u.username })),
|
||||
[users]
|
||||
);
|
||||
|
||||
const dayMap = useMemo(() => {
|
||||
const m = new Map<string, { created: number; done: number }>();
|
||||
if (!data) return m;
|
||||
for (const d of data.created_daily) {
|
||||
const cur = m.get(d.date) ?? { created: 0, done: 0 };
|
||||
cur.created = d.count;
|
||||
m.set(d.date, cur);
|
||||
}
|
||||
for (const d of data.throughput_daily) {
|
||||
const cur = m.get(d.date) ?? { created: 0, done: 0 };
|
||||
cur.done = d.count;
|
||||
m.set(d.date, cur);
|
||||
}
|
||||
return m;
|
||||
}, [data]);
|
||||
|
||||
// Build month grid (Mon-first).
|
||||
const grid = useMemo(() => {
|
||||
const start = dayjs(month).startOf("month");
|
||||
const end = dayjs(month).endOf("month");
|
||||
// Day-of-week, ISO Mon=1..Sun=7. We want first cell to be Mon.
|
||||
const firstDow = (start.day() + 6) % 7; // 0=Mon
|
||||
const cells: { date: string | null; inMonth: boolean }[] = [];
|
||||
for (let i = 0; i < firstDow; i++) cells.push({ date: null, inMonth: false });
|
||||
for (let d = start; !d.isAfter(end); d = d.add(1, "day")) {
|
||||
cells.push({ date: d.format("YYYY-MM-DD"), inMonth: true });
|
||||
}
|
||||
while (cells.length % 7 !== 0) cells.push({ date: null, inMonth: false });
|
||||
return cells;
|
||||
}, [month]);
|
||||
|
||||
const totalCreated = useMemo(
|
||||
() => Array.from(dayMap.values()).reduce((s, v) => s + v.created, 0),
|
||||
[dayMap]
|
||||
);
|
||||
const totalDone = useMemo(
|
||||
() => Array.from(dayMap.values()).reduce((s, v) => s + v.done, 0),
|
||||
[dayMap]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box p="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Title order={3}>Calendario</Title>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<MonthPickerInput
|
||||
label="Mes"
|
||||
size="xs"
|
||||
value={month}
|
||||
onChange={(v) => v && setMonth(typeof v === "string" ? new Date(v) : v)}
|
||||
style={{ minWidth: 160 }}
|
||||
clearable={false}
|
||||
/>
|
||||
<Select
|
||||
label="Asignado"
|
||||
size="xs"
|
||||
placeholder="Todos"
|
||||
value={assigneeId}
|
||||
onChange={setAssigneeId}
|
||||
data={userOptions}
|
||||
clearable
|
||||
searchable
|
||||
style={{ minWidth: 180 }}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Group gap="md">
|
||||
<Paper withBorder p="sm" radius="md">
|
||||
<Group gap={6}>
|
||||
<IconPlus size={14} color="var(--mantine-color-blue-5)" />
|
||||
<Text size="sm" fw={600}>
|
||||
{totalCreated}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
creadas
|
||||
</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
<Paper withBorder p="sm" radius="md">
|
||||
<Group gap={6}>
|
||||
<IconCheckbox size={14} color="var(--mantine-color-green-5)" />
|
||||
<Text size="sm" fw={600}>
|
||||
{totalDone}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
hechas
|
||||
</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Group>
|
||||
|
||||
{loading && !data ? (
|
||||
<Center p="xl">
|
||||
<Loader />
|
||||
</Center>
|
||||
) : (
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<SimpleGrid cols={7} spacing={4} mb={4}>
|
||||
{DAY_LABELS.map((d) => (
|
||||
<Text key={d} size="xs" c="dimmed" ta="center" fw={600}>
|
||||
{d}
|
||||
</Text>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<SimpleGrid cols={7} spacing={4}>
|
||||
{grid.map((cell, i) => {
|
||||
if (!cell.date) {
|
||||
return <Box key={i} style={{ minHeight: 72 }} />;
|
||||
}
|
||||
const stats = dayMap.get(cell.date) ?? { created: 0, done: 0 };
|
||||
const dayNum = parseInt(cell.date.slice(8, 10), 10);
|
||||
const isToday = cell.date === dayjs().format("YYYY-MM-DD");
|
||||
return (
|
||||
<Paper
|
||||
key={i}
|
||||
p={6}
|
||||
withBorder
|
||||
radius="sm"
|
||||
style={{
|
||||
minHeight: 72,
|
||||
borderColor: isToday ? "var(--mantine-color-blue-5)" : undefined,
|
||||
background:
|
||||
stats.done > 0
|
||||
? "rgba(81, 207, 102, 0.08)"
|
||||
: stats.created > 0
|
||||
? "rgba(34, 139, 230, 0.06)"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<Text size="xs" fw={isToday ? 700 : 500} c={isToday ? "blue" : undefined}>
|
||||
{dayNum}
|
||||
</Text>
|
||||
{stats.created > 0 && (
|
||||
<Group gap={3} wrap="nowrap">
|
||||
<IconPlus size={10} color="var(--mantine-color-blue-5)" />
|
||||
<Text size="xs" c="blue">
|
||||
{stats.created}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
{stats.done > 0 && (
|
||||
<Group gap={3} wrap="nowrap">
|
||||
<IconCheckbox size={10} color="var(--mantine-color-green-5)" />
|
||||
<Text size="xs" c="green">
|
||||
{stats.done}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user