e792bc6e17
Backend (server/main.py): - GET /api/calendars: lista las colecciones de calendario bajo el calendar-home con nombre y color (compone dav_list_calendars del registry). - GET /api/calendar?cal=&from=&to=: eventos de una colección concreta (caché por colección validada por ctag). dtstart/dtend ahora en ISO con offset + tz original + all_day; parseo robusto de TZID/UTC/todo-el-día con zoneinfo. - POST/PUT/DELETE /api/event[/<uid>]: CRUD de VEVENT contra Xandikos (fuente de verdad). Construye el VCALENDAR (con VTIMEZONE para zonas con DST), reutiliza el UID al editar (idempotente), trata 404 del DELETE como idempotente, invalida la caché de la colección tras escribir. Frontend: - CalendarView reescrita: conmutador Mes/Semana/Día con rejilla horaria propia (Mantine + dayjs, sin react-big-calendar para evitar fricción con React 19), mini-calendario de navegación, selector de calendario (con color), selector de zona horaria que recoloca los eventos, colores por evento (del VEVENT o del calendario). - EventModal: alta/edición/borrado con summary, inicio/fin, todo-el-día, TZ, calendario, color, ubicación y descripción. Fechas en formato local 24h. - calendar.ts: helpers de TZ (dayjs utc+timezone), posicionado por hora, semana empezando en lunes, locale es. api.ts: tipos y funciones de eventos/calendarios. Verificado: ciclo real crear→editar→borrar contra Xandikos (cero residuo), render del calendario en navegador (React 19 + Mantine v9 montan), pnpm build verde, 40 tests verdes (+ smoke gateado). MKCALENDAR queda fuera (documentado). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
695 lines
20 KiB
TypeScript
695 lines
20 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from "react";
|
||
import {
|
||
ActionIcon,
|
||
Alert,
|
||
Badge,
|
||
Box,
|
||
Button,
|
||
Center,
|
||
ColorSwatch,
|
||
Group,
|
||
Indicator,
|
||
Loader,
|
||
Paper,
|
||
ScrollArea,
|
||
SegmentedControl,
|
||
Select,
|
||
Stack,
|
||
Text,
|
||
Title,
|
||
Tooltip,
|
||
} from "@mantine/core";
|
||
import { Calendar } from "@mantine/dates";
|
||
import { notifications } from "@mantine/notifications";
|
||
import {
|
||
IconChevronLeft,
|
||
IconChevronRight,
|
||
IconPlus,
|
||
} from "@tabler/icons-react";
|
||
import {
|
||
deleteEvent,
|
||
fetchCalendar,
|
||
fetchCalendars,
|
||
type CalendarCollection,
|
||
type CalendarEvent,
|
||
} from "../api";
|
||
import {
|
||
dayjs,
|
||
eventColor,
|
||
eventStart,
|
||
HOURS,
|
||
monthMatrix,
|
||
positionEventsForDay,
|
||
TIMEZONES,
|
||
WEEKDAY_LABELS,
|
||
weekDays,
|
||
browserTz,
|
||
} from "../calendar";
|
||
import { EventModal, type EventDraft } from "./EventModal";
|
||
|
||
type ViewMode = "mes" | "semana" | "dia";
|
||
|
||
// Calendario: vista Mes/Semana/Día con eventos posicionados por hora, selector
|
||
// de calendario (con color), selector de zona horaria, colores por evento y CRUD
|
||
// completo (crear en un hueco, editar/borrar un evento). El mini-calendario de la
|
||
// izquierda navega; el cuerpo muestra la vista activa en la TZ seleccionada.
|
||
|
||
export function CalendarView() {
|
||
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
||
const [calendars, setCalendars] = useState<CalendarCollection[]>([]);
|
||
const [activeCal, setActiveCal] = useState<string>("");
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [view, setView] = useState<ViewMode>("semana");
|
||
const [tz, setTz] = useState<string>(browserTz());
|
||
// `cursor` es el día de referencia (la vista lo expande a su mes/semana/día).
|
||
const [cursor, setCursor] = useState<string>(dayjs().format("YYYY-MM-DD"));
|
||
const [modal, setModal] = useState<EventDraft | null>(null);
|
||
const [reloadKey, setReloadKey] = useState(0);
|
||
|
||
// Carga de calendarios (selector) una vez.
|
||
useEffect(() => {
|
||
let alive = true;
|
||
fetchCalendars()
|
||
.then((d) => {
|
||
if (!alive) return;
|
||
if (d.status === "ok" && d.calendars) {
|
||
setCalendars(d.calendars);
|
||
if (!activeCal && d.default) setActiveCal(d.default);
|
||
else if (!activeCal && d.calendars[0]) setActiveCal(d.calendars[0].href);
|
||
}
|
||
})
|
||
.catch(() => {
|
||
/* el selector degrada a "calendario" implícito; no es fatal */
|
||
});
|
||
return () => {
|
||
alive = false;
|
||
};
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, []);
|
||
|
||
// Carga de eventos de la colección activa.
|
||
useEffect(() => {
|
||
let alive = true;
|
||
setLoading(true);
|
||
setError(null);
|
||
fetchCalendar(activeCal)
|
||
.then((d) => {
|
||
if (!alive) return;
|
||
if (d.status !== "ok") {
|
||
setError(d.error || "Xandikos no respondió");
|
||
return;
|
||
}
|
||
setEvents(d.events ?? []);
|
||
})
|
||
.catch((e) => alive && setError(String(e)))
|
||
.finally(() => alive && setLoading(false));
|
||
return () => {
|
||
alive = false;
|
||
};
|
||
}, [activeCal, reloadKey]);
|
||
|
||
const calColor = useMemo(() => {
|
||
const c = calendars.find((c) => c.href === activeCal);
|
||
return c?.color ?? null;
|
||
}, [calendars, activeCal]);
|
||
|
||
// Días con eventos (para el punto en el mini-calendario), en la TZ activa.
|
||
const daysWithEvents = useMemo(() => {
|
||
const set = new Set<string>();
|
||
for (const e of events) {
|
||
const s = eventStart(e, tz);
|
||
if (s) set.add(s.format("YYYY-MM-DD"));
|
||
}
|
||
return set;
|
||
}, [events, tz]);
|
||
|
||
const reload = useCallback(() => setReloadKey((k) => k + 1), []);
|
||
|
||
const onSaved = useCallback(
|
||
(msg: string) => {
|
||
notifications.show({ color: "teal", title: "Calendario", message: msg });
|
||
setModal(null);
|
||
reload();
|
||
},
|
||
[reload],
|
||
);
|
||
|
||
const onDelete = useCallback(
|
||
async (uid: string) => {
|
||
try {
|
||
await deleteEvent(uid, activeCal);
|
||
notifications.show({
|
||
color: "teal",
|
||
title: "Evento borrado",
|
||
message: "",
|
||
});
|
||
setModal(null);
|
||
reload();
|
||
} catch (e) {
|
||
notifications.show({
|
||
color: "red",
|
||
title: "No se pudo borrar",
|
||
message: String(e),
|
||
});
|
||
}
|
||
},
|
||
[activeCal, reload],
|
||
);
|
||
|
||
// Abre el modal en modo "nuevo" en un hueco (día + hora opcional).
|
||
const openNew = useCallback(
|
||
(day: string, hour?: number) => {
|
||
const start =
|
||
hour === undefined
|
||
? `${day}T09:00`
|
||
: `${day}T${String(hour).padStart(2, "0")}:00`;
|
||
const endHour = hour === undefined ? 10 : Math.min(hour + 1, 23);
|
||
const end =
|
||
hour === undefined
|
||
? `${day}T10:00`
|
||
: `${day}T${String(endHour).padStart(2, "0")}:00`;
|
||
setModal({
|
||
mode: "new",
|
||
summary: "",
|
||
dtstart: start,
|
||
dtend: end,
|
||
tz,
|
||
all_day: false,
|
||
location: "",
|
||
description: "",
|
||
color: "",
|
||
cal: activeCal,
|
||
});
|
||
},
|
||
[tz, activeCal],
|
||
);
|
||
|
||
// Abre el modal en modo "editar" desde un evento existente.
|
||
const openEdit = useCallback(
|
||
(ev: CalendarEvent) => {
|
||
const s = eventStart(ev, tz);
|
||
const e = ev.dtend ? eventStart({ ...ev, dtstart: ev.dtend }, tz) : null;
|
||
setModal({
|
||
mode: "edit",
|
||
uid: ev.uid ?? undefined,
|
||
summary: ev.summary ?? "",
|
||
dtstart: ev.all_day
|
||
? (ev.dtstart ?? "").slice(0, 10)
|
||
: (s?.format("YYYY-MM-DDTHH:mm") ?? ""),
|
||
dtend: ev.all_day
|
||
? (ev.dtend ?? "").slice(0, 10)
|
||
: (e?.format("YYYY-MM-DDTHH:mm") ?? ""),
|
||
tz: ev.tz || tz,
|
||
all_day: !!ev.all_day,
|
||
location: ev.location ?? "",
|
||
description: ev.description ?? "",
|
||
color: ev.color ?? "",
|
||
cal: activeCal,
|
||
});
|
||
},
|
||
[tz, activeCal],
|
||
);
|
||
|
||
function navigate(dir: -1 | 1) {
|
||
const unit = view === "mes" ? "month" : view === "semana" ? "week" : "day";
|
||
setCursor(dayjs(cursor).add(dir, unit).format("YYYY-MM-DD"));
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<Center h="100%" p="xl">
|
||
<Alert color="orange" title="Calendario no disponible" maw={500}>
|
||
{error}
|
||
<Text size="sm" mt="xs" c="dimmed">
|
||
El calendario viene del servidor Xandikos. El resto de la app (grafo,
|
||
tablas) funciona sin él.
|
||
</Text>
|
||
</Alert>
|
||
</Center>
|
||
);
|
||
}
|
||
|
||
const headerTitle =
|
||
view === "mes"
|
||
? dayjs(cursor).format("MMMM YYYY")
|
||
: view === "semana"
|
||
? (() => {
|
||
const wd = weekDays(dayjs(cursor));
|
||
return `${wd[0].format("D MMM")} – ${wd[6].format("D MMM YYYY")}`;
|
||
})()
|
||
: dayjs(cursor).format("dddd D [de] MMMM YYYY");
|
||
|
||
return (
|
||
<Group h="100%" gap={0} wrap="nowrap" align="stretch">
|
||
{/* Panel izquierdo: navegación + selectores */}
|
||
<Paper
|
||
p="md"
|
||
radius={0}
|
||
withBorder
|
||
style={{ borderTop: 0, borderBottom: 0, borderLeft: 0, width: 280 }}
|
||
>
|
||
<Stack gap="md">
|
||
<Button
|
||
leftSection={<IconPlus size={16} />}
|
||
onClick={() => openNew(cursor)}
|
||
fullWidth
|
||
>
|
||
Nuevo evento
|
||
</Button>
|
||
|
||
<Calendar
|
||
date={cursor}
|
||
onDateChange={(d) => setCursor(d)}
|
||
getDayProps={(date) => ({
|
||
selected: cursor === date,
|
||
onClick: () => {
|
||
setCursor(date);
|
||
if (view === "mes") setView("dia");
|
||
},
|
||
})}
|
||
renderDay={(date) => {
|
||
const has = daysWithEvents.has(date);
|
||
const day = Number(date.slice(8, 10));
|
||
return (
|
||
<Indicator size={6} color="brand" offset={-2} disabled={!has}>
|
||
<div>{day}</div>
|
||
</Indicator>
|
||
);
|
||
}}
|
||
/>
|
||
|
||
<Select
|
||
label="Calendario"
|
||
data={calendars.map((c) => ({ value: c.href, label: c.name }))}
|
||
value={activeCal || null}
|
||
onChange={(v) => v && setActiveCal(v)}
|
||
leftSection={
|
||
<ColorSwatch
|
||
size={14}
|
||
color={eventColor({} as CalendarEvent, calColor)}
|
||
/>
|
||
}
|
||
allowDeselect={false}
|
||
comboboxProps={{ withinPortal: true }}
|
||
/>
|
||
|
||
<Select
|
||
label="Zona horaria"
|
||
data={TIMEZONES}
|
||
value={tz}
|
||
onChange={(v) => v && setTz(v)}
|
||
searchable
|
||
comboboxProps={{ withinPortal: true }}
|
||
/>
|
||
|
||
<Text size="xs" c="dimmed">
|
||
{events.length} eventos · TZ {tz}
|
||
</Text>
|
||
</Stack>
|
||
</Paper>
|
||
|
||
{/* Cuerpo: barra de vista + rejilla */}
|
||
<Box style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
||
<Group justify="space-between" p="sm" wrap="nowrap" style={{ flexShrink: 0 }}>
|
||
<Group gap="xs" wrap="nowrap">
|
||
<Tooltip label="Anterior">
|
||
<ActionIcon variant="default" onClick={() => navigate(-1)}>
|
||
<IconChevronLeft size={16} />
|
||
</ActionIcon>
|
||
</Tooltip>
|
||
<Button
|
||
variant="default"
|
||
size="xs"
|
||
onClick={() => setCursor(dayjs().format("YYYY-MM-DD"))}
|
||
>
|
||
Hoy
|
||
</Button>
|
||
<Tooltip label="Siguiente">
|
||
<ActionIcon variant="default" onClick={() => navigate(1)}>
|
||
<IconChevronRight size={16} />
|
||
</ActionIcon>
|
||
</Tooltip>
|
||
<Title order={4} style={{ textTransform: "capitalize" }}>
|
||
{headerTitle}
|
||
</Title>
|
||
</Group>
|
||
<SegmentedControl
|
||
value={view}
|
||
onChange={(v) => setView(v as ViewMode)}
|
||
data={[
|
||
{ value: "mes", label: "Mes" },
|
||
{ value: "semana", label: "Semana" },
|
||
{ value: "dia", label: "Día" },
|
||
]}
|
||
/>
|
||
</Group>
|
||
|
||
<Box style={{ flex: 1, minHeight: 0 }}>
|
||
{loading ? (
|
||
<Center h="100%">
|
||
<Loader />
|
||
</Center>
|
||
) : view === "mes" ? (
|
||
<MonthView
|
||
cursor={cursor}
|
||
events={events}
|
||
tz={tz}
|
||
calColor={calColor}
|
||
onDay={(d) => {
|
||
setCursor(d);
|
||
setView("dia");
|
||
}}
|
||
onEvent={openEdit}
|
||
/>
|
||
) : view === "semana" ? (
|
||
<TimeGrid
|
||
days={weekDays(dayjs(cursor))}
|
||
events={events}
|
||
tz={tz}
|
||
calColor={calColor}
|
||
onSlot={openNew}
|
||
onEvent={openEdit}
|
||
/>
|
||
) : (
|
||
<TimeGrid
|
||
days={[dayjs(cursor)]}
|
||
events={events}
|
||
tz={tz}
|
||
calColor={calColor}
|
||
onSlot={openNew}
|
||
onEvent={openEdit}
|
||
/>
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
|
||
<EventModal
|
||
draft={modal}
|
||
calendars={calendars}
|
||
onClose={() => setModal(null)}
|
||
onSaved={onSaved}
|
||
onDelete={onDelete}
|
||
/>
|
||
</Group>
|
||
);
|
||
}
|
||
|
||
// --- Vista de semana / día: rejilla horas × días ---------------------------
|
||
|
||
function TimeGrid({
|
||
days,
|
||
events,
|
||
tz,
|
||
calColor,
|
||
onSlot,
|
||
onEvent,
|
||
}: {
|
||
days: dayjs.Dayjs[];
|
||
events: CalendarEvent[];
|
||
tz: string;
|
||
calColor: string | null;
|
||
onSlot: (day: string, hour: number) => void;
|
||
onEvent: (ev: CalendarEvent) => void;
|
||
}) {
|
||
const HOUR_PX = 44;
|
||
const today = dayjs().format("YYYY-MM-DD");
|
||
return (
|
||
<ScrollArea h="100%" type="auto">
|
||
<Box style={{ display: "flex", minWidth: days.length > 1 ? 640 : 0 }}>
|
||
{/* Columna de horas */}
|
||
<Box style={{ width: 52, flexShrink: 0 }}>
|
||
<Box style={{ height: 28 }} />
|
||
{HOURS.map((h) => (
|
||
<Box
|
||
key={h}
|
||
style={{
|
||
height: HOUR_PX,
|
||
textAlign: "right",
|
||
paddingRight: 6,
|
||
fontSize: 11,
|
||
color: "var(--mantine-color-dimmed)",
|
||
transform: "translateY(-7px)",
|
||
}}
|
||
>
|
||
{String(h).padStart(2, "0")}:00
|
||
</Box>
|
||
))}
|
||
</Box>
|
||
|
||
{/* Una columna por día */}
|
||
{days.map((day) => {
|
||
const dayKey = day.format("YYYY-MM-DD");
|
||
const positioned = positionEventsForDay(events, dayKey, tz);
|
||
const allDay = positioned.filter((p) => p.allDay);
|
||
const timed = positioned.filter((p) => !p.allDay);
|
||
const isToday = dayKey === today;
|
||
return (
|
||
<Box
|
||
key={dayKey}
|
||
style={{
|
||
flex: 1,
|
||
minWidth: 0,
|
||
borderLeft: "1px solid var(--mantine-color-default-border)",
|
||
}}
|
||
>
|
||
{/* Cabecera del día */}
|
||
<Box
|
||
style={{
|
||
height: 28,
|
||
textAlign: "center",
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
color: isToday
|
||
? "var(--mantine-color-brand-6)"
|
||
: "var(--mantine-color-text)",
|
||
}}
|
||
>
|
||
{WEEKDAY_LABELS[(day.day() + 6) % 7]} {day.format("D")}
|
||
</Box>
|
||
|
||
{/* Franja de "todo el día" */}
|
||
{allDay.length > 0 && (
|
||
<Box style={{ padding: "2px 3px" }}>
|
||
{allDay.map((p, i) => (
|
||
<EventChip
|
||
key={(p.ev.uid ?? "") + i}
|
||
ev={p.ev}
|
||
calColor={calColor}
|
||
onClick={() => onEvent(p.ev)}
|
||
compact
|
||
/>
|
||
))}
|
||
</Box>
|
||
)}
|
||
|
||
{/* Rejilla horaria con posicionado absoluto */}
|
||
<Box style={{ position: "relative", height: HOUR_PX * 24 }}>
|
||
{HOURS.map((h) => (
|
||
<Box
|
||
key={h}
|
||
onClick={() => onSlot(dayKey, h)}
|
||
style={{
|
||
position: "absolute",
|
||
top: h * HOUR_PX,
|
||
left: 0,
|
||
right: 0,
|
||
height: HOUR_PX,
|
||
borderTop: "1px solid var(--mantine-color-default-border)",
|
||
cursor: "pointer",
|
||
}}
|
||
/>
|
||
))}
|
||
{timed.map((p, i) => (
|
||
<Box
|
||
key={(p.ev.uid ?? "") + i}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onEvent(p.ev);
|
||
}}
|
||
style={{
|
||
position: "absolute",
|
||
top: `${p.topPct}%`,
|
||
height: `${p.heightPct}%`,
|
||
left: 2,
|
||
right: 2,
|
||
overflow: "hidden",
|
||
borderRadius: 4,
|
||
padding: "1px 5px",
|
||
fontSize: 11,
|
||
lineHeight: 1.25,
|
||
color: "#fff",
|
||
background: eventColor(p.ev, calColor),
|
||
cursor: "pointer",
|
||
}}
|
||
>
|
||
<Text size="xs" fw={600} truncate c="#fff">
|
||
{eventStart(p.ev, tz)?.format("HH:mm")} ·{" "}
|
||
{p.ev.summary || "(sin título)"}
|
||
</Text>
|
||
</Box>
|
||
))}
|
||
</Box>
|
||
</Box>
|
||
);
|
||
})}
|
||
</Box>
|
||
</ScrollArea>
|
||
);
|
||
}
|
||
|
||
// --- Vista de mes: rejilla de semanas --------------------------------------
|
||
|
||
function MonthView({
|
||
cursor,
|
||
events,
|
||
tz,
|
||
calColor,
|
||
onDay,
|
||
onEvent,
|
||
}: {
|
||
cursor: string;
|
||
events: CalendarEvent[];
|
||
tz: string;
|
||
calColor: string | null;
|
||
onDay: (day: string) => void;
|
||
onEvent: (ev: CalendarEvent) => void;
|
||
}) {
|
||
const weeks = monthMatrix(dayjs(cursor));
|
||
const month = dayjs(cursor).month();
|
||
const today = dayjs().format("YYYY-MM-DD");
|
||
|
||
const byDay = useMemo(() => {
|
||
const map = new Map<string, CalendarEvent[]>();
|
||
for (const e of events) {
|
||
const s = eventStart(e, tz);
|
||
if (!s) continue;
|
||
const k = s.format("YYYY-MM-DD");
|
||
const list = map.get(k) ?? [];
|
||
list.push(e);
|
||
map.set(k, list);
|
||
}
|
||
return map;
|
||
}, [events, tz]);
|
||
|
||
return (
|
||
<ScrollArea h="100%">
|
||
<Box style={{ display: "flex", padding: "0 8px" }}>
|
||
{WEEKDAY_LABELS.map((l) => (
|
||
<Box
|
||
key={l}
|
||
style={{
|
||
flex: 1,
|
||
textAlign: "center",
|
||
fontSize: 11,
|
||
fontWeight: 600,
|
||
color: "var(--mantine-color-dimmed)",
|
||
padding: "4px 0",
|
||
}}
|
||
>
|
||
{l}
|
||
</Box>
|
||
))}
|
||
</Box>
|
||
<Stack gap={0} px={8} pb={8}>
|
||
{weeks.map((week, wi) => (
|
||
<Group key={wi} gap={0} wrap="nowrap" align="stretch">
|
||
{week.map((day) => {
|
||
const k = day.format("YYYY-MM-DD");
|
||
const list = (byDay.get(k) ?? []).sort((a, b) =>
|
||
(a.dtstart ?? "").localeCompare(b.dtstart ?? ""),
|
||
);
|
||
const dim = day.month() !== month;
|
||
const isToday = k === today;
|
||
return (
|
||
<Box
|
||
key={k}
|
||
onClick={() => onDay(k)}
|
||
style={{
|
||
flex: 1,
|
||
minWidth: 0,
|
||
minHeight: 92,
|
||
border: "1px solid var(--mantine-color-default-border)",
|
||
padding: 3,
|
||
cursor: "pointer",
|
||
background: dim
|
||
? "var(--mantine-color-default-hover)"
|
||
: undefined,
|
||
}}
|
||
>
|
||
<Text
|
||
size="xs"
|
||
ta="right"
|
||
fw={isToday ? 700 : 400}
|
||
c={
|
||
isToday
|
||
? "brand"
|
||
: dim
|
||
? "dimmed"
|
||
: undefined
|
||
}
|
||
>
|
||
{day.format("D")}
|
||
</Text>
|
||
<Stack gap={2} mt={2}>
|
||
{list.slice(0, 3).map((ev, i) => (
|
||
<EventChip
|
||
key={(ev.uid ?? "") + i}
|
||
ev={ev}
|
||
calColor={calColor}
|
||
onClick={() => onEvent(ev)}
|
||
compact
|
||
/>
|
||
))}
|
||
{list.length > 3 && (
|
||
<Text size="xs" c="dimmed">
|
||
+{list.length - 3} más
|
||
</Text>
|
||
)}
|
||
</Stack>
|
||
</Box>
|
||
);
|
||
})}
|
||
</Group>
|
||
))}
|
||
</Stack>
|
||
</ScrollArea>
|
||
);
|
||
}
|
||
|
||
// Chip compacto de un evento (mes / franja all-day).
|
||
function EventChip({
|
||
ev,
|
||
calColor,
|
||
onClick,
|
||
compact,
|
||
}: {
|
||
ev: CalendarEvent;
|
||
calColor: string | null;
|
||
onClick: () => void;
|
||
compact?: boolean;
|
||
}) {
|
||
const color = eventColor(ev, calColor);
|
||
return (
|
||
<Badge
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onClick();
|
||
}}
|
||
variant="filled"
|
||
radius="sm"
|
||
fullWidth
|
||
style={{
|
||
background: color,
|
||
cursor: "pointer",
|
||
justifyContent: "flex-start",
|
||
textTransform: "none",
|
||
fontWeight: 500,
|
||
height: compact ? 16 : 20,
|
||
}}
|
||
>
|
||
{ev.summary || "(sin título)"}
|
||
</Badge>
|
||
);
|
||
}
|