b172a8f087
Las 11 pantallas con listado/detalle bloqueaban TODO su render con "if (loading || !data) return <Cargando/>". Como useAsync pone loading=true en cada reload (no solo en la carga inicial), cualquier accion dentro de un DetailPopup abierto que dispara un reload del padre (agregar subtarea, agregar dependencia, guardar comentario, etc.) desmontaba momentaneamente TODA la pantalla -y con ella el popup y su estado interno (pestaña activa)-, que volvia a montarse desde cero con sus valores iniciales. Sintoma reportado: al agregar una subtarea (Enter o boton "Nuevo"), el popup de la actividad volvia a la pestaña Info en vez de quedarse en Subtareas. Fix: la guarda ahora es solo "if (!data)" -bloquea unicamente la carga inicial, cuando todavia no hay nada que mostrar-. Las recargas subsiguientes actualizan el contenido en el sitio sin desmontar nada. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
67 lines
2.9 KiB
React
67 lines
2.9 KiB
React
import { useMemo, useState } from 'react';
|
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
import { useUi } from '../../context/ui-context.jsx';
|
|
import { useAsync } from '../../hooks/useAsync.js';
|
|
import { listActivitiesByProject } from '../../services/activities.js';
|
|
import { Button } from '../../components/Button.jsx';
|
|
|
|
const DOW = ['Lun', 'Mar', 'Mie', 'Jue', 'Vie', 'Sab', 'Dom'];
|
|
|
|
function buildGrid(year, month) {
|
|
const first = new Date(year, month, 1);
|
|
const startOffset = (first.getDay() + 6) % 7; // lunes = 0
|
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
const cells = [];
|
|
for (let i = 0; i < startOffset; i++) cells.push(null);
|
|
for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(year, month, d));
|
|
while (cells.length % 7 !== 0) cells.push(null);
|
|
return cells;
|
|
}
|
|
|
|
export default function ProjectCalendarTab({ projectId }) {
|
|
const { t } = useUi();
|
|
const { data: activities } = useAsync(() => listActivitiesByProject(projectId), [projectId]);
|
|
const [cursor, setCursor] = useState(() => { const n = new Date(); return new Date(n.getFullYear(), n.getMonth(), 1); });
|
|
|
|
const cells = useMemo(() => buildGrid(cursor.getFullYear(), cursor.getMonth()), [cursor]);
|
|
const todayStr = new Date().toISOString().slice(0, 10);
|
|
|
|
const byDay = useMemo(() => {
|
|
const map = {};
|
|
for (const a of activities || []) {
|
|
if (!a.due_date) continue;
|
|
(map[a.due_date] ||= []).push(a);
|
|
}
|
|
return map;
|
|
}, [activities]);
|
|
|
|
if (!activities) return <div className="text-muted">{t('common_loading')}</div>;
|
|
|
|
return (
|
|
<div>
|
|
<div className="toolbar">
|
|
<Button variant="standard" size="sm" icon={ChevronLeft} onClick={() => setCursor(new Date(cursor.getFullYear(), cursor.getMonth() - 1, 1))} />
|
|
<div style={{ fontWeight: 700 }}>{cursor.toLocaleDateString('es-EC', { month: 'long', year: 'numeric' })}</div>
|
|
<Button variant="standard" size="sm" icon={ChevronRight} onClick={() => setCursor(new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1))} />
|
|
</div>
|
|
<div className="calendar-grid">
|
|
{DOW.map((d) => <div key={d} className="calendar-dow">{d}</div>)}
|
|
{cells.map((date, i) => {
|
|
if (!date) return <div key={i} className="calendar-cell muted" />;
|
|
const key = date.toISOString().slice(0, 10);
|
|
const items = byDay[key] || [];
|
|
return (
|
|
<div key={i} className={`calendar-cell ${key === todayStr ? 'today' : ''}`}>
|
|
<div className="calendar-day-num">{date.getDate()}</div>
|
|
{items.slice(0, 3).map((a) => (
|
|
<div key={a.id} className="calendar-chip" title={a.name}>{a.is_milestone ? '◆ ' : ''}{a.name}</div>
|
|
))}
|
|
{items.length > 3 && <div className="text-muted" style={{ fontSize: '.64rem' }}>+{items.length - 3}</div>}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|