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:
Milton Gonzalez
2026-09-03 08:37:58 -05:00
commit 1d22adc42e
135 changed files with 10913 additions and 0 deletions
@@ -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>
);
}