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
+36
View File
@@ -0,0 +1,36 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from '../context/auth-context.jsx';
import { UiProvider } from '../context/ui-context.jsx';
import { ContentDialogHost } from '../components/ContentDialog.jsx';
import { ToastHost } from '../components/ToastHost.jsx';
import { ProtectedRoute } from './ProtectedRoute.jsx';
import AppShell from './AppShell.jsx';
import LoginPage from '../pages/Login/LoginPage.jsx';
import DashboardPage from '../pages/Dashboard/DashboardPage.jsx';
import OrganizacionPage from '../pages/Organizacion/OrganizacionPage.jsx';
import ProyectosPage from '../pages/Proyectos/ProyectosPage.jsx';
import ProyectoDetallePage from '../pages/Proyectos/ProyectoDetallePage.jsx';
import ConfiguracionPage from '../pages/Configuracion/ConfiguracionPage.jsx';
export default function App() {
return (
<UiProvider>
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<ProtectedRoute><AppShell /></ProtectedRoute>}>
<Route index element={<DashboardPage />} />
<Route path="proyectos" element={<ProyectosPage />} />
<Route path="proyectos/:id" element={<ProyectoDetallePage />} />
<Route path="organizacion" element={<OrganizacionPage />} />
<Route path="configuracion" element={<ConfiguracionPage />} />
</Route>
</Routes>
</BrowserRouter>
<ContentDialogHost />
<ToastHost />
</AuthProvider>
</UiProvider>
);
}
+94
View File
@@ -0,0 +1,94 @@
import { useState } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
import { Menu, Sun, Moon, Bell, LogOut, User, Globe } from 'lucide-react';
import { NavigationView } from '../components/NavigationView.jsx';
import { Flyout, FlyoutItem, FlyoutSeparator } from '../components/Flyout.jsx';
import { useAuth } from '../context/auth-context.jsx';
import { useUi } from '../context/ui-context.jsx';
import { useAsync } from '../hooks/useAsync.js';
import { listNotifications, markAllNotificationsRead, markNotificationRead } from '../services/notifications.js';
export default function AppShell() {
const [navMode, setNavMode] = useState('expanded');
const { user, logout } = useAuth();
const { t, theme, setTheme, lang, setLang } = useUi();
const navigate = useNavigate();
const { data: notifData, reload: reloadNotifications } = useAsync(listNotifications, []);
const unread = notifData?.unread || 0;
function toggleNav() {
setNavMode((m) => (m === 'expanded' ? 'compact' : 'expanded'));
}
async function handleNotificationClick(notification) {
if (!notification.is_read) await markNotificationRead(notification.id);
await reloadNotifications();
if (notification.entity_type === 'project') navigate(`/proyectos/${notification.entity_id}`);
else if (notification.entity_type === 'activity') navigate(`/proyectos`);
}
return (
<div className="app-shell">
<NavigationView mode={navMode} />
<div className="content-column">
<div className="topbar">
<div className="topbar-left">
<button className="icon-btn" onClick={toggleNav}><Menu /></button>
</div>
<div className="topbar-right">
<Flyout trigger={({ onClick }) => (
<button className="icon-btn" onClick={onClick} style={{ position: 'relative' }}>
<Bell />
{unread > 0 && (
<span className="badge badge-danger" style={{ position: 'absolute', top: -2, right: -2, padding: '1px 5px', fontSize: '.6rem' }}>{unread}</span>
)}
</button>
)}>
<div style={{ padding: '6px 10px', fontWeight: 700, fontSize: '.78rem' }}>{t('notifications_title')}</div>
{(!notifData?.items || notifData.items.length === 0) && (
<div style={{ padding: '10px', fontSize: '.78rem', color: 'rgb(var(--muted))' }}>{t('notifications_empty')}</div>
)}
{notifData?.items?.slice(0, 8).map((n) => (
<FlyoutItem key={n.id} onClick={() => handleNotificationClick(n)}>
<span style={{ fontWeight: n.is_read ? 400 : 700 }}>{n.message}</span>
</FlyoutItem>
))}
{notifData?.items?.length > 0 && (
<>
<FlyoutSeparator />
<FlyoutItem onClick={async () => { await markAllNotificationsRead(); reloadNotifications(); }}>
{t('notifications_mark_all_read')}
</FlyoutItem>
</>
)}
</Flyout>
<button className="icon-btn" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? <Sun /> : <Moon />}
</button>
<button className="icon-btn" onClick={() => setLang(lang === 'es-EC' ? 'en-US' : 'es-EC')} title={lang}>
<Globe />
</button>
<Flyout trigger={({ onClick }) => (
<button className="icon-btn" onClick={onClick}><User /></button>
)}>
<div style={{ padding: '8px 10px' }}>
<div style={{ fontWeight: 700, fontSize: '.82rem' }}>{user?.name}</div>
<div style={{ fontSize: '.7rem', color: 'rgb(var(--muted))' }}>{user?.roleName || user?.roleCode}</div>
</div>
<FlyoutSeparator />
<FlyoutItem icon={LogOut} danger onClick={() => { logout(); navigate('/login'); }}>
{t('logout')}
</FlyoutItem>
</Flyout>
</div>
</div>
<div className="content-area">
<Outlet />
</div>
</div>
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/auth-context.jsx';
export function ProtectedRoute({ children }) {
const { user, ready } = useAuth();
if (!ready) return null;
if (!user) return <Navigate to="/login" replace />;
return children;
}