chore: auto-commit (10 archivos)

- chat.log
- db.go
- frontend/src/App.tsx
- frontend/src/api.ts
- frontend/src/components/CardForm.tsx
- frontend/src/components/Dashboard.tsx
- frontend/src/components/KanbanCard.tsx
- frontend/src/types.ts
- handlers.go
- metrics.go

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 15:55:35 +02:00
parent 9290a0b2d0
commit 2a727eb7c1
10 changed files with 583 additions and 52 deletions
+42 -8
View File
@@ -1,4 +1,4 @@
import { Button, Group, Select, Stack, Textarea, TextInput } from "@mantine/core";
import { Autocomplete, Button, Group, Select, Stack, TagsInput, Textarea } from "@mantine/core";
import { FormEvent, KeyboardEvent, useState } from "react";
import type { User } from "../types";
@@ -7,21 +7,33 @@ export interface CardFormValues {
title: string;
description: string;
assignee_id: string | null;
tags: string[];
}
interface Props {
initial?: Partial<CardFormValues>;
submitLabel?: string;
users?: User[];
requesterOptions?: string[];
tagOptions?: string[];
onSubmit: (v: CardFormValues) => Promise<void> | void;
onCancel: () => void;
}
export function CardForm({ initial, submitLabel = "Guardar", users = [], onSubmit, onCancel }: Props) {
export function CardForm({
initial,
submitLabel = "Guardar",
users = [],
requesterOptions = [],
tagOptions = [],
onSubmit,
onCancel,
}: Props) {
const [requester, setRequester] = useState(initial?.requester ?? "");
const [title, setTitle] = useState(initial?.title ?? "");
const [description, setDescription] = useState(initial?.description ?? "");
const [assigneeId, setAssigneeId] = useState<string | null>(initial?.assignee_id ?? null);
const [tags, setTags] = useState<string[]>(initial?.tags ?? []);
const submit = async (e?: FormEvent) => {
e?.preventDefault();
@@ -32,6 +44,7 @@ export function CardForm({ initial, submitLabel = "Guardar", users = [], onSubmi
title: t,
description,
assignee_id: assigneeId,
tags,
});
};
@@ -51,7 +64,7 @@ export function CardForm({ initial, submitLabel = "Guardar", users = [], onSubmi
return (
<form onSubmit={submit}>
<Stack gap="sm">
<TextInput
<Textarea
label="Tarea"
value={title}
onChange={(e) => setTitle(e.currentTarget.value)}
@@ -59,15 +72,26 @@ export function CardForm({ initial, submitLabel = "Guardar", users = [], onSubmi
required
autoComplete="off"
data-autofocus
onKeyDown={enterSubmit}
autosize
minRows={1}
maxRows={4}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
submit();
}
}}
/>
<TextInput
<Autocomplete
label="Solicitante"
value={requester}
onChange={(e) => setRequester(e.currentTarget.value)}
onChange={setRequester}
data={requesterOptions}
tabIndex={2}
autoComplete="off"
onKeyDown={enterSubmit}
placeholder="Empieza a escribir y elige uno existente"
limit={10}
/>
<Textarea
label="Descripcion"
@@ -93,11 +117,21 @@ export function CardForm({ initial, submitLabel = "Guardar", users = [], onSubmi
searchable
tabIndex={4}
/>
<TagsInput
label="Tags"
value={tags}
onChange={setTags}
data={tagOptions}
clearable
tabIndex={5}
placeholder="Enter para añadir; sugiere existentes"
splitChars={[",", " "]}
/>
<Group justify="flex-end" gap="xs" mt="xs">
<Button variant="subtle" color="gray" tabIndex={6} type="button" onClick={onCancel}>
<Button variant="subtle" color="gray" tabIndex={7} type="button" onClick={onCancel}>
Cancelar
</Button>
<Button tabIndex={5} type="submit" disabled={!title.trim()}>
<Button tabIndex={6} type="submit" disabled={!title.trim()}>
{submitLabel}
</Button>
</Group>
+20 -7
View File
@@ -178,7 +178,8 @@ export function Dashboard({ users }: Props) {
if (!data) return [];
return data.top_requesters.map((r) => ({
solicitante: r.requester,
tarjetas: r.total,
activas: r.active,
completadas: r.completed_in_range,
}));
}, [data]);
@@ -250,18 +251,25 @@ export function Dashboard({ users }: Props) {
{data && (
<>
<SimpleGrid cols={{ base: 2, md: 4 }} spacing="md">
<SimpleGrid cols={{ base: 2, md: 5 }} spacing="md">
<KPI
icon={<IconClipboardList size={14} />}
label="Tarjetas totales"
label="Totales"
value={data.totals.cards}
hint={`${data.totals.columns} columnas, ${data.totals.users} usuarios`}
/>
<KPI
icon={<IconClipboardList size={14} />}
label="Activas"
value={data.totals.cards_active}
hint={`Sin completar`}
color="blue"
/>
<KPI
icon={<IconCheckbox size={14} />}
label="Completadas (rango)"
value={data.totals.cards_completed_in_range}
hint={`${data.totals.cards_created_in_range} creadas en rango`}
hint={`${data.totals.cards_done} completadas total · ${data.totals.cards_created_in_range} creadas rango`}
color="green"
/>
<KPI
@@ -426,12 +434,17 @@ export function Dashboard({ users }: Props) {
</Text>
) : (
<BarChart
h={240}
h={Math.max(240, topRequesterSeries.length * 32)}
data={topRequesterSeries}
dataKey="solicitante"
orientation="vertical"
yAxisProps={{ width: 120 }}
series={[{ name: "tarjetas", label: "Tarjetas", color: "violet.6" }]}
yAxisProps={{ width: 160, interval: 0 }}
withLegend
series={[
{ name: "completadas", label: "Completadas", color: "green.6" },
{ name: "activas", label: "Activas", color: "violet.6" },
]}
type="stacked"
/>
)}
</Paper>
+11
View File
@@ -149,6 +149,7 @@ function KanbanCardImpl({
position="right-start"
withArrow
shadow="md"
withinPortal={false}
>
<Popover.Target>
<Menu.Item
@@ -176,6 +177,7 @@ function KanbanCardImpl({
clearable
searchable
autoFocus
comboboxProps={{ withinPortal: false }}
/>
</Popover.Dropdown>
</Popover>
@@ -298,6 +300,15 @@ function KanbanCardImpl({
{card.description}
</Text>
)}
{card.tags && card.tags.length > 0 && (
<Group gap={4} wrap="wrap">
{card.tags.map((t) => (
<Badge key={t} size="xs" variant="outline" color="violet" radius="sm">
{t}
</Badge>
))}
</Group>
)}
<Group gap={4}>
<Badge size="xs" variant="light" color="gray" leftSection={<IconClock size={10} />}>
{formatDuration(liveMs)}