fix(calendar): vista lista sin recurrentes repetidos + secciones próximos/pasados
La vista de lista pedía el rango 2000-2100, así que el backend expandía cada cumpleaños anual ~75 veces (hasta 2099). Ahora: (1) el rango llega solo a +13 meses, suficiente para la próxima ocurrencia de cada serie; (2) AgendaView deduplica las series recurrentes a una sola entrada (su próxima ocurrencia); y (3) separa la agenda en 'Próximos' (>= hoy, ascendente) arriba y 'Pasados' (< hoy, descendente) debajo, estos atenuados (opacity 0.5) en gris claro. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -107,11 +107,13 @@ export function CalendarView() {
|
|||||||
to: c.add(1, "day").format("YYYY-MM-DD"),
|
to: c.add(1, "day").format("YYYY-MM-DD"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// lista: TODOS los eventos del calendario (pasados y futuros). Rango muy
|
// lista: histórico completo de eventos puntuales (desde 2000) + las series
|
||||||
// amplio para que el backend expanda cualquier serie recurrente y no recorte.
|
// recurrentes expandidas solo hasta ~13 meses vista. Ese tope evita que un
|
||||||
|
// cumpleaños anual se expanda hasta 2099; AgendaView deduplica además cada
|
||||||
|
// serie a una sola entrada (su próxima ocurrencia).
|
||||||
return {
|
return {
|
||||||
from: "2000-01-01",
|
from: "2000-01-01",
|
||||||
to: "2100-01-01",
|
to: dayjs().add(13, "month").format("YYYY-MM-DD"),
|
||||||
};
|
};
|
||||||
}, [cursor, view]);
|
}, [cursor, view]);
|
||||||
|
|
||||||
@@ -936,52 +938,81 @@ function AgendaView({
|
|||||||
onNew: () => void;
|
onNew: () => void;
|
||||||
onEvent: (ev: CalendarEvent) => void;
|
onEvent: (ev: CalendarEvent) => void;
|
||||||
}) {
|
}) {
|
||||||
// Orden ascendente por instante de inicio + agrupado por día (clave YYYY-MM-DD).
|
const startOfToday = useMemo(() => dayjs().tz(tz).startOf("day"), [tz]);
|
||||||
const groups = useMemo(() => {
|
const todayKey = startOfToday.format("YYYY-MM-DD");
|
||||||
const sorted = [...events].sort((a, b) => {
|
|
||||||
const sa = eventStart(a, tz);
|
// 1) Deduplica las series recurrentes: una sola entrada por uid (su próxima
|
||||||
const sb = eventStart(b, tz);
|
// ocurrencia desde hoy; si todas son pasadas, la más reciente). Así un
|
||||||
const va = sa ? sa.valueOf() : Number.POSITIVE_INFINITY;
|
// cumpleaños anual aparece una vez, no una por año. Los puntuales se dejan.
|
||||||
const vb = sb ? sb.valueOf() : Number.POSITIVE_INFINITY;
|
const deduped = useMemo(() => {
|
||||||
return va - vb;
|
const recurring = new Map<string, CalendarEvent[]>();
|
||||||
|
const singles: CalendarEvent[] = [];
|
||||||
|
for (const ev of events) {
|
||||||
|
if (ev.recurring && ev.uid) {
|
||||||
|
const l = recurring.get(ev.uid) ?? [];
|
||||||
|
l.push(ev);
|
||||||
|
recurring.set(ev.uid, l);
|
||||||
|
} else {
|
||||||
|
singles.push(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const out = [...singles];
|
||||||
|
for (const occ of recurring.values()) {
|
||||||
|
const sorted = occ
|
||||||
|
.slice()
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(eventStart(a, tz)?.valueOf() ?? 0) -
|
||||||
|
(eventStart(b, tz)?.valueOf() ?? 0),
|
||||||
|
);
|
||||||
|
const next = sorted.find((e) => {
|
||||||
|
const s = eventStart(e, tz);
|
||||||
|
return s && !s.isBefore(startOfToday);
|
||||||
});
|
});
|
||||||
|
out.push(next ?? sorted[sorted.length - 1]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}, [events, tz, startOfToday]);
|
||||||
|
|
||||||
|
// 2) Separa en próximos (>= hoy, ascendente) y pasados (< hoy, descendente),
|
||||||
|
// cada uno agrupado por día.
|
||||||
|
const groupByDay = (list: CalendarEvent[]) => {
|
||||||
const map = new Map<string, CalendarEvent[]>();
|
const map = new Map<string, CalendarEvent[]>();
|
||||||
for (const ev of sorted) {
|
for (const ev of list) {
|
||||||
const s = eventStart(ev, tz);
|
const s = eventStart(ev, tz);
|
||||||
if (!s) continue;
|
if (!s) continue;
|
||||||
const k = s.format("YYYY-MM-DD");
|
const k = s.format("YYYY-MM-DD");
|
||||||
const list = map.get(k) ?? [];
|
const arr = map.get(k) ?? [];
|
||||||
list.push(ev);
|
arr.push(ev);
|
||||||
map.set(k, list);
|
map.set(k, arr);
|
||||||
}
|
}
|
||||||
return Array.from(map.entries());
|
return Array.from(map.entries());
|
||||||
}, [events, tz]);
|
};
|
||||||
|
|
||||||
const today = dayjs().tz(tz).format("YYYY-MM-DD");
|
const { upcomingGroups, pastGroups } = useMemo(() => {
|
||||||
|
const upcoming: CalendarEvent[] = [];
|
||||||
|
const past: CalendarEvent[] = [];
|
||||||
|
for (const ev of deduped) {
|
||||||
|
const s = eventStart(ev, tz);
|
||||||
|
if (s && s.isBefore(startOfToday)) past.push(ev);
|
||||||
|
else upcoming.push(ev);
|
||||||
|
}
|
||||||
|
upcoming.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(eventStart(a, tz)?.valueOf() ?? Infinity) -
|
||||||
|
(eventStart(b, tz)?.valueOf() ?? Infinity),
|
||||||
|
);
|
||||||
|
past.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(eventStart(b, tz)?.valueOf() ?? 0) -
|
||||||
|
(eventStart(a, tz)?.valueOf() ?? 0),
|
||||||
|
);
|
||||||
|
return { upcomingGroups: groupByDay(upcoming), pastGroups: groupByDay(past) };
|
||||||
|
}, [deduped, tz, startOfToday]);
|
||||||
|
|
||||||
return (
|
const renderGroup = ([dayKey, list]: [string, CalendarEvent[]]) => {
|
||||||
<ScrollArea h="100%" type="auto">
|
|
||||||
<Box p="md">
|
|
||||||
<Group justify="space-between" mb="sm" wrap="nowrap">
|
|
||||||
<Text fw={600}>Todos los eventos</Text>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
leftSection={<IconPlus size={14} />}
|
|
||||||
onClick={onNew}
|
|
||||||
>
|
|
||||||
Nuevo evento
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{groups.length === 0 ? (
|
|
||||||
<Text c="dimmed" size="sm">
|
|
||||||
No hay eventos en este calendario.
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Stack gap="lg">
|
|
||||||
{groups.map(([dayKey, list]) => {
|
|
||||||
const d = dayjs(dayKey);
|
const d = dayjs(dayKey);
|
||||||
const isToday = dayKey === today;
|
const isToday = dayKey === todayKey;
|
||||||
return (
|
return (
|
||||||
<Box key={dayKey}>
|
<Box key={dayKey}>
|
||||||
<Text
|
<Text
|
||||||
@@ -1016,11 +1047,7 @@ function AgendaView({
|
|||||||
background: "var(--mantine-color-default-hover)",
|
background: "var(--mantine-color-default-hover)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text size="xs" c="dimmed" style={{ width: 96, flexShrink: 0 }}>
|
||||||
size="xs"
|
|
||||||
c="dimmed"
|
|
||||||
style={{ width: 96, flexShrink: 0 }}
|
|
||||||
>
|
|
||||||
{timeLabel}
|
{timeLabel}
|
||||||
</Text>
|
</Text>
|
||||||
<Group gap={4} wrap="nowrap" style={{ minWidth: 0 }}>
|
<Group gap={4} wrap="nowrap" style={{ minWidth: 0 }}>
|
||||||
@@ -1037,7 +1064,49 @@ function AgendaView({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
})}
|
};
|
||||||
|
|
||||||
|
const isEmpty = upcomingGroups.length === 0 && pastGroups.length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea h="100%" type="auto">
|
||||||
|
<Box p="md">
|
||||||
|
<Group justify="space-between" mb="sm" wrap="nowrap">
|
||||||
|
<Text fw={600}>Agenda</Text>
|
||||||
|
<Button size="xs" leftSection={<IconPlus size={14} />} onClick={onNew}>
|
||||||
|
Nuevo evento
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{isEmpty ? (
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
No hay eventos en este calendario.
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Stack gap="xl">
|
||||||
|
{/* Próximos */}
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb="sm">
|
||||||
|
Próximos
|
||||||
|
</Text>
|
||||||
|
{upcomingGroups.length === 0 ? (
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
No hay eventos próximos.
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Stack gap="lg">{upcomingGroups.map(renderGroup)}</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Pasados — atenuados en gris claro */}
|
||||||
|
{pastGroups.length > 0 && (
|
||||||
|
<Box style={{ opacity: 0.5 }}>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb="sm">
|
||||||
|
Pasados
|
||||||
|
</Text>
|
||||||
|
<Stack gap="lg">{pastGroups.map(renderGroup)}</Stack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user