diff --git a/ROADMAP.md b/ROADMAP.md
index d67bb6c..a8eb814 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -77,20 +77,22 @@
### FASE 3: FRONTEND NEXT.JS (~6-8 semanas)
-- [ ] **3.1 Setup** (create-next-app, packages, config)
-- [ ] **3.2 Layout Principal** (~1 sem)
- - [ ] layout.tsx (sidebar + header = SAM.Master)
- - [ ] Sidebar.tsx, Header.tsx
- - [ ] CSS original (style.css, ichn.css, etc.)
-- [ ] **3.3 Login + Auth** (~3 días)
-- [ ] **3.4 Dashboard** (~3 días)
-- [ ] **3.5 Módulo Leads** (~1 sem) — 3 páginas
-- [ ] **3.6 Módulo Cotizaciones/Pagos** (~1 sem)
-- [ ] **3.7 Módulo Empresas** (~1 sem) — 3 páginas
-- [ ] **3.8 Resto páginas** (~2-3 sem)
- - Arqueo, Cursos, Alumnos
+- [x] **3.1 Setup** (Next.js 15, TypeScript, packages)
+- [x] **3.2 Layout Principal**
+ - [x] layout.tsx (sidebar + header = SAM.Master)
+ - [x] Sidebar.tsx, Header.tsx, LoadingSpinner, ModalConfirmacion
+ - [x] CSS original copiado + globals.css con sidebar styles
+ - [x] Assets (favicon, ichnHD, samito, Invertido_ICN)
+- [x] **3.3 Login + Auth** (login page + useAuth hook + JWT)
+- [x] **3.4 Dashboard** (cards layout)
+- [x] **3.5 JS migration** (validarRut.ts, utils.ts, useInactividad)
+- [x] **3.6 api.ts** (centralized HTTP client with cookies)
+- [x] **Dockerfile Frontend** (multi-stage build)
+- [ ] **3.5-3.8 Páginas restantes** (~20 páginas)
+ - Leads (listado, detalle, nuevo)
+ - Cotizaciones, Arqueo, Cursos, Alumnos
+ - Empresas (listado, detalle, ventas)
- Reportes (3), Autorizador, Contacto
-- [ ] Dockerfile Frontend
### FASE 4: CONTENEDORES (~1 semana)
@@ -120,7 +122,8 @@
| 2026-07-08 | F1 | 13 controllers, Ejecutivo entity, ArqueoService, JwtMiddleware, SpComplexQueries | Fase 2 |
| 2026-07-08 | F1.7 | Reportes restantes: Anexo, ContratoBlack, Presupuesto, CAEMP/CAEMPSNC, CC_EMPCSD, PropuestaComercial | Fase 2 |
| 2026-07-08 | F2 | ServicesExternos: DTE, Transbank, Email + API | Fase 3 |
-| 2026-07-08 | AUDIT | 27 bugs corregidos: JWT secret, QuestPDF license, schema SPs, Transbank MySQL, JwtMiddleware pipeline, Email disconnect, model validation, casts | Fase 3 |
+| 2026-07-08 | AUDIT | 27 bugs corregidos en F1+F2 | Fase 3 |
+| 2026-07-08 | F3 | Setup Next.js + Layout + Sidebar + Login + Auth + Dashboard + componentes + CSS | Resto páginas frontend |
| | | | |
| | | | |
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 0000000..5ca29a3
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,14 @@
+FROM node:20-alpine AS build
+WORKDIR /app
+COPY package*.json ./
+RUN npm ci
+COPY . .
+RUN npm run build
+
+FROM node:20-alpine AS runtime
+WORKDIR /app
+COPY --from=build /app/.next/standalone ./
+COPY --from=build /app/public ./public
+EXPOSE 3000
+ENV NODE_ENV=production
+CMD ["node", "server.js"]
diff --git a/frontend/next.config.js b/frontend/next.config.js
new file mode 100644
index 0000000..dd5147e
--- /dev/null
+++ b/frontend/next.config.js
@@ -0,0 +1,9 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ output: 'standalone',
+ images: {
+ unoptimized: true,
+ },
+};
+
+module.exports = nextConfig;
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..45a9c91
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "modulo-ventas",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "next": "^15.0.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-hot-toast": "^2.4.0"
+ },
+ "devDependencies": {
+ "@types/node": "^22.0.0",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "typescript": "^5.0.0"
+ }
+}
diff --git a/frontend/public/css/ichn.css b/frontend/public/css/ichn.css
new file mode 100644
index 0000000..db1e1d6
--- /dev/null
+++ b/frontend/public/css/ichn.css
@@ -0,0 +1,230 @@
+
+
+.btn-ichn {
+ color: #fff;
+ background-color: #00285d;
+ border-color: #00285d
+}
+
+
+ .btn-ichn:hover {
+ color: #fff;
+ background-color: #00142E;
+ border-color: #00142E
+ }
+
+ .btn-ichn.focus, .btn-ichn:focus {
+ box-shadow: 0 0 0 .2rem #00285d
+ }
+
+ .btn-ichn.disabled, .btn-ichn:disabled {
+ color: #fff;
+ background-color: #00285d;
+ border-color: #00285d;
+ }
+
+ .btn-ichn:not(:disabled):not(.disabled).active, .btn-ichn:not(:disabled):not(.disabled):active, .show > .btn-ichn.dropdown-toggle {
+ color: #fff;
+ background-color: #00285d;
+ border-color: #00285d
+ }
+
+ .btn-ichn:not(:disabled):not(.disabled).active:focus, .btn-ichn:not(:disabled):not(.disabled):active:focus, .show > .btn-ichn.dropdown-toggle:focus {
+ box-shadow: 0 0 0 .2rem #00285d
+ }
+
+.btn-ichnRojo {
+ color: #fff;
+ background-color: #e22523;
+ border-color: #e22523
+}
+
+
+ .btn-ichnRojo:hover {
+ color: #fff;
+ background-color: #8A101C;
+ border-color: #8A101C
+ }
+
+ .btn-ichnRojo.focus, .btn-ichnRojo:focus {
+ box-shadow: 0 0 0 .2rem #B91425
+ }
+
+ .btn-ichnRojo.disabled, .btn-ichnRojo:disabled {
+ color: #fff;
+ background-color: #B91425;
+ border-color: #B91425;
+ }
+
+ .btn-ichnRojo:not(:disabled):not(.disabled).active, .btn-ichnRojo:not(:disabled):not(.disabled):active, .show > .btn-ichnRojo.dropdown-toggle {
+ color: #fff;
+ background-color: #B91425;
+ border-color: #B91425
+ }
+
+ .btn-ichnRojo:not(:disabled):not(.disabled).active:focus, .btn-ichnRojo:not(:disabled):not(.disabled):active:focus, .show > .btn-ichnRojo.dropdown-toggle:focus {
+ box-shadow: 0 0 0 .2rem #B91425
+ }
+
+
+.btn-ichnPurpura {
+ color: #fff;
+ background-color: #6B3B7C;
+ border-color: #6B3B7C;
+}
+
+ .btn-ichnPurpura:hover {
+ color: #fff;
+ background-color: #562965;
+ border-color: #562965
+ }
+
+ .btn-ichnPurpura.focus, .btn-ichnPurpura:focus {
+ box-shadow: 0 0 0 .2rem #844c98
+ }
+
+ .btn-ichnPurpura.disabled, .btn-ichnPurpura:disabled {
+ color: #fff;
+ background-color: #844c98;
+ border-color: #844c98;
+ }
+
+ .btn-ichnPurpura:not(:disabled):not(.disabled).active, .btn-ichnPurpura:not(:disabled):not(.disabled):active, .show > .btn-ichnPurpura.dropdown-toggle {
+ color: #fff;
+ background-color: #844c98;
+ border-color: #844c98
+ }
+
+ .btn-ichnPurpura:not(:disabled):not(.disabled).active:focus, .btn-ichnPurpura:not(:disabled):not(.disabled):active:focus, .show > .btn-ichnPurpura.dropdown-toggle:focus {
+ box-shadow: 0 0 0 .2rem #844c98
+ }
+
+
+.btn-ichnVerde {
+ color: #fff;
+ background-color: #3D912F;
+ border-color: #3D912F
+}
+
+
+ .btn-ichnVerde:hover {
+ color: #fff;
+ background-color: #2A6D1E;
+ border-color: #2A6D1E
+ }
+
+ .btn-ichnVerde.focus, .btn-ichnVerde:focus {
+ box-shadow: 0 0 0 .2rem #3D912F
+ }
+
+ .btn-ichnVerde.disabled, .btn-ichnVerde:disabled {
+ color: #fff;
+ background-color: #3D912F;
+ border-color: #3D912F;
+ }
+
+ .btn-ichnVerde:not(:disabled):not(.disabled).active, .btn-ichnVerde:not(:disabled):not(.disabled):active, .show > .btn-ichnVerde.dropdown-toggle {
+ color: #fff;
+ background-color: #3D912F;
+ border-color: #3D912F
+ }
+
+ .btn-ichnVerde:not(:disabled):not(.disabled).active:focus, .btn-ichnVerde:not(:disabled):not(.disabled):active:focus, .show > .btn-ichnVerde.dropdown-toggle:focus {
+ box-shadow: 0 0 0 .2rem #3D912F
+ }
+
+
+.btn-ichnExcel {
+ color: #fff;
+ background-color: #008000;
+ border-color: #008000
+}
+
+
+ .btn-ichnExcel:hover {
+ color: #fff;
+ background-color: #009900;
+ border-color: #009900;
+ }
+
+ .btn-ichnExcel.focus, .btn-ichnExcel:focus {
+ box-shadow: 0 0 0 .2rem #008000;
+ }
+
+ .btn-ichnExcel.disabled, .btn-ichnExcel:disabled {
+ color: #fff;
+ background-color: #008000;
+ border-color: #008000;
+ }
+
+ .btn-ichnExcel:not(:disabled):not(.disabled).active, .btn-ichnExcel:not(:disabled):not(.disabled):active, .show > .btn-ichnExcel.dropdown-toggle {
+ color: #fff;
+ background-color: #008000;
+ border-color: #008000
+ }
+
+ .btn-ichnExcel:not(:disabled):not(.disabled).active:focus, .btn-ichnExcel:not(:disabled):not(.disabled):active:focus, .show > .btn-ichnExcel.dropdown-toggle:focus {
+ box-shadow: 0 0 0 .2rem #008000
+ }
+
+
+.examen:hover {
+ background-image: linear-gradient( rgba(185, 20, 37 ), rgba(185, 20, 37) )
+}
+
+.modal-content {
+ position: relative;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ width: 100%;
+ pointer-events: auto;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 0px solid;
+ border-radius: .5rem;
+ outline: 0;
+}
+
+.modal-header {
+ display: -ms-flexbox;
+ display: block;
+ -ms-flex-align: start;
+ align-items: flex-start;
+ -ms-flex-pack: justify;
+ justify-content: space-between;
+ padding: 1rem 1rem;
+ border-bottom: 1px solid #dee2e6;
+ border-top-left-radius: .3rem;
+ border-top-right-radius: .3rem;
+}
+
+.btn-circle {
+ width: 30px;
+ height: 30px;
+ text-align: center;
+ padding: 6px 0;
+ font-size: 12px;
+ line-height: 1.428571429;
+ border-radius: 15px;
+}
+
+ .btn-circle.btn-lg {
+ width: 50px;
+ height: 50px;
+ padding: 10px 16px;
+ font-size: 18px;
+ line-height: 1.33;
+ border-radius: 25px;
+ }
+
+ .btn-circle.btn-xl {
+ width: 70px;
+ height: 70px;
+ padding: 10px 16px;
+ font-size: 24px;
+ line-height: 1.33;
+ border-radius: 35px;
+ }
+
diff --git a/frontend/public/css/ichnPaginas.css b/frontend/public/css/ichnPaginas.css
new file mode 100644
index 0000000..f012cb3
--- /dev/null
+++ b/frontend/public/css/ichnPaginas.css
@@ -0,0 +1,80 @@
+.div_1 {
+ height: 80px;
+ background-color: #D1D1D1;
+ font-size: 18px;
+}
+
+.div_2 {
+ min-height: 300px;
+}
+
+label .prueba {
+ cursor: pointer;
+}
+
+.nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link.active {
+ color: #ffff;
+ background-color: #22376c;
+ border-color: #dee2e6 #dee2e6 #fff;
+}
+
+.nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link {
+ color: #22376c;
+}
+
+.spinner, .spinner:after {
+ width: 90px;
+ height: 90px;
+ position: fixed;
+ top: 90%;
+ left: 50%;
+ margin-top: -32px;
+ margin-left: -32px;
+ border-radius: 50%;
+ z-index: 2
+}
+
+.spinner {
+ background-color: transparent;
+ border-top: 10px solid rgb(0, 40, 93);
+ border-right: 10px solid rgb(0, 40, 93);
+ border-bottom: 10px solid rgb(0, 40, 93);
+ border-left: 10px solid rgba(0, 40, 93,.2);
+ transform: translateZ(0);
+ animation-iteration-count: infinite;
+ animation-timing-function: linear;
+ animation-duration: 1.5s;
+ animation-name: spinner-loading
+}
+
+.menuIchnHover {
+ background-color: #E8E8E8;
+}
+
+ .menuIchnHover a {
+ color: #212529;
+ }
+
+ .menuIchnHover:hover {
+ background-color: #00285d;
+ color: #fff;
+ border-radius: 5px
+ }
+
+
+ .menuIchnHover:hover a {
+ background-color: #00285d;
+ color: #fff;
+ border-radius: 5px
+ }
+
+
+@keyframes spinner-loading {
+ 0% {
+ transform: rotate(0deg)
+ }
+
+ to {
+ transform: rotate(1turn)
+ }
+}
\ No newline at end of file
diff --git a/frontend/public/css/style.css b/frontend/public/css/style.css
new file mode 100644
index 0000000..0d9fe3b
--- /dev/null
+++ b/frontend/public/css/style.css
@@ -0,0 +1,282 @@
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+.sidebar {
+ position: fixed;
+ left: 0;
+ top: 0;
+ height: 100%;
+ width: 75px;
+ background: #00285d;
+ padding: 6px 14px;
+ z-index: 99;
+ transition: all 0.5s ease;
+}
+
+ .sidebar.open {
+ width: 250px;
+ }
+
+ .sidebar .logo-details {
+ height: 60px;
+ display: flex;
+ align-items: center;
+ position: relative;
+ }
+
+ .sidebar .logo-details .icon {
+ opacity: 0;
+ transition: all 0.5s ease;
+ }
+
+ .sidebar .logo-details .logo_name {
+ color: #fff;
+ font-size: 20px;
+ font-weight: 600;
+ opacity: 0;
+ transition: all 0.5s ease;
+ }
+
+ .sidebar.open .logo-details .icon,
+ .sidebar.open .logo-details .logo_name {
+ opacity: 1;
+ }
+
+ .sidebar .logo-details #btn {
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
+ font-size: 22px;
+ transition: all 0.4s ease;
+ font-size: 23px;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.5s ease;
+ }
+
+ .sidebar.open .logo-details #btn {
+ text-align: right;
+ }
+
+ .sidebar i {
+ color: #fff;
+ height: 60px;
+ min-width: 50px;
+ font-size: 28px;
+ text-align: center;
+ line-height: 60px;
+ }
+
+ .sidebar .nav-list {
+ margin-top: 20px;
+ height: 100%;
+ padding-left: 0rem;
+ }
+
+ .sidebar li {
+ position: relative;
+ margin: 8px 0;
+ list-style: none;
+ }
+
+ .sidebar li .tooltip {
+ position: absolute;
+ top: -20px;
+ left: calc(100% + 15px);
+ z-index: 3;
+ background: #fff;
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
+ padding: 6px 12px;
+ border-radius: 4px;
+ font-size: 15px;
+ font-weight: 400;
+ opacity: 0;
+ white-space: nowrap;
+ pointer-events: none;
+ transition: 0s;
+ }
+
+ .sidebar li:hover .tooltip {
+ opacity: 1;
+ pointer-events: auto;
+ transition: all 0.4s ease;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+
+ .sidebar.open li .tooltip {
+ display: none;
+ }
+
+ .sidebar input {
+ font-size: 15px;
+ color: #FFF;
+ font-weight: 400;
+ outline: none;
+ height: 50px;
+ width: 100%;
+ width: 50px;
+ border: none;
+ border-radius: 12px;
+ transition: all 0.5s ease;
+ background: #1d1b31;
+ }
+
+ .sidebar.open input {
+ padding: 0 20px 0 50px;
+ width: 100%;
+ }
+
+ .sidebar .bx-search {
+ position: absolute;
+ top: 50%;
+ left: 0;
+ transform: translateY(-50%);
+ font-size: 22px;
+ background: #1d1b31;
+ color: #FFF;
+ }
+
+ .sidebar.open .bx-search:hover {
+ background: #1d1b31;
+ color: #FFF;
+ }
+
+ .sidebar .bx-search:hover {
+ background: #FFF;
+ color: #00285d;
+ }
+
+ .sidebar li a {
+ display: flex;
+ height: 100%;
+ width: 100%;
+ border-radius: 12px;
+ align-items: center;
+ text-decoration: none;
+ transition: all 0.4s ease;
+ background: #00285d;
+ }
+
+ .sidebar li a:hover {
+ background: #FFF;
+ }
+
+ .sidebar li a .links_name {
+ color: #fff;
+ font-size: 15px;
+ font-weight: 400;
+ white-space: nowrap;
+ opacity: 0;
+ pointer-events: none;
+ transition: 0.4s;
+ }
+
+ .sidebar.open li a .links_name {
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ .sidebar li a:hover .links_name,
+ .sidebar li a:hover i {
+ transition: all 0.5s ease;
+ color: #00285d;
+ }
+
+ .sidebar li i {
+ height: 35px;
+ line-height: 35px;
+ font-size: 16px;
+ border-radius: 12px;
+ }
+
+ .sidebar li.profile {
+ position: fixed;
+ height: 60px;
+ width: 78px;
+ left: 0;
+ bottom: -8px;
+ padding: 10px 14px;
+ background: #001D44;
+ transition: all 0.5s ease;
+ overflow: hidden;
+ }
+
+ .sidebar.open li.profile {
+ width: 250px;
+ }
+
+ .sidebar li .profile-details {
+ display: flex;
+ align-items: center;
+ flex-wrap: nowrap;
+ }
+
+ .sidebar li img {
+ height: 45px;
+ width: 45px;
+ object-fit: cover;
+ border-radius: 6px;
+ margin-right: 10px;
+ }
+
+ .sidebar li.profile .name,
+ .sidebar li.profile .job {
+ font-size: 15px;
+ font-weight: 400;
+ color: #fff;
+ white-space: nowrap;
+ }
+
+ .sidebar li.profile .job {
+ font-size: 12px;
+ }
+
+ .sidebar .profile #log_out {
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
+ background: #1d1b31;
+ width: 100%;
+ height: 60px;
+ line-height: 60px;
+ border-radius: 0px;
+ transition: all 0.5s ease;
+ }
+
+ .sidebar.open .profile #log_out {
+ width: 50px;
+ background: none;
+ }
+
+.home-section {
+ position: relative;
+ min-height: 100vh;
+ top: 0;
+ left: 75px;
+ width: calc(100% - 75px);
+ transition: all 0.5s ease;
+}
+
+.sidebar.open ~ .home-section {
+ left: 250px;
+ width: calc(100% - 250px);
+ transition: all 1s ease;
+}
+
+.home-section .text {
+ display: inline-block;
+ margin: 18px
+}
+
+@media (max-width: 620px) {
+ .sidebar li .tooltip {
+ display: none;
+ }
+}
diff --git a/frontend/public/img/Invertido_ICN.png b/frontend/public/img/Invertido_ICN.png
new file mode 100644
index 0000000..45cd452
Binary files /dev/null and b/frontend/public/img/Invertido_ICN.png differ
diff --git a/frontend/public/img/favicon.png b/frontend/public/img/favicon.png
new file mode 100644
index 0000000..7ce4c34
Binary files /dev/null and b/frontend/public/img/favicon.png differ
diff --git a/frontend/public/img/ichnHD.png b/frontend/public/img/ichnHD.png
new file mode 100644
index 0000000..fd7aa03
Binary files /dev/null and b/frontend/public/img/ichnHD.png differ
diff --git a/frontend/public/img/samito.png b/frontend/public/img/samito.png
new file mode 100644
index 0000000..af4b795
Binary files /dev/null and b/frontend/public/img/samito.png differ
diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx
new file mode 100644
index 0000000..9129933
--- /dev/null
+++ b/frontend/src/app/dashboard/page.tsx
@@ -0,0 +1,52 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { useAuth } from '@/hooks/useAuth';
+import { api } from '@/services/api';
+import LoadingSpinner from '@/components/LoadingSpinner';
+
+const cards = [
+ { title: 'Nuevos Leads', icon: 'fas fa-users', color: '#4e73df', endpoint: '/lead/nuevos' },
+ { title: 'Cotizaciones', icon: 'fas fa-file-invoice-dollar', color: '#1cc88a', endpoint: '/cotizacion/buscar' },
+ { title: 'Cursos', icon: 'fas fa-book', color: '#36b9cc', endpoint: '/curso' },
+ { title: 'Alumnos', icon: 'fas fa-user-graduate', color: '#f6c23e', endpoint: '/alumno' },
+];
+
+export default function DashboardPage() {
+ const { user, loading } = useAuth();
+ const router = useRouter();
+ const [pageLoading, setPageLoading] = useState(true);
+
+ useEffect(() => {
+ if (!loading && !user) router.push('/login');
+ if (!loading) setPageLoading(false);
+ }, [user, loading, router]);
+
+ if (pageLoading) return
+ {text}
+{message}
+