chore: estructura inicial del MVP
Backend (Express + MariaDB) y frontend (React + Vite, identidad Fluent/Windows) con el nucleo funcional: seguridad, organizacion, proyectos, actividades, subtareas, dependencias, dashboard, calendario, gantt, comentarios, adjuntos, notificaciones in-app y catalogos. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
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, loading } = 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 (loading || !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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user