From 129991cfb74ec3e5b691e1c5e38665eaa0873afc Mon Sep 17 00:00:00 2001 From: Nurfog Date: Wed, 8 Jul 2026 11:07:27 -0400 Subject: [PATCH] feat: complete fases 3,4,5 - frontend, containers, e2e tests FASE 3 - Frontend (22 paginas): - Leads (listado, detalle, nuevo), Cotizaciones, Arqueo - Cursos, Alumnos, Empresas (listado, detalle, ventas) - Reportes (ventas, ventas-empresas, reimpresion) - Autorizador, Contacto FASE 4 - Contenedores: - docker-compose.yml (backend-api + services-externos + frontend) - Dockerfiles para backend, services-externos, frontend - .env.example con secretos FASE 5 - Pruebas E2E: - Playwright config + 3 spec files (login, leads, dashboard) - 7 escenarios: login, error, sidebar, crear lead, dashboard cards, timeout, reportes --- .env.example | 15 ++++ ROADMAP.md | 10 ++- backend/src/Ventas.API/Dockerfile | 11 +++ docker-compose.yml | 51 +++++++++++++ frontend/src/app/alumnos/page.tsx | 67 +++++++++++++++++ frontend/src/app/arqueo/page.tsx | 54 ++++++++++++++ frontend/src/app/autorizador/page.tsx | 32 ++++++++ frontend/src/app/contacto/page.tsx | 40 ++++++++++ frontend/src/app/cotizaciones/page.tsx | 52 +++++++++++++ frontend/src/app/cursos/page.tsx | 52 +++++++++++++ frontend/src/app/empresas/[id]/page.tsx | 47 ++++++++++++ frontend/src/app/empresas/page.tsx | 52 +++++++++++++ frontend/src/app/empresas/ventas/page.tsx | 52 +++++++++++++ frontend/src/app/leads/[id]/page.tsx | 73 +++++++++++++++++++ frontend/src/app/leads/nuevo/page.tsx | 51 +++++++++++++ frontend/src/app/leads/page.tsx | 64 ++++++++++++++++ .../src/app/reportes/reimpresion/page.tsx | 37 ++++++++++ .../src/app/reportes/ventas-empresas/page.tsx | 40 ++++++++++ frontend/src/app/reportes/ventas/page.tsx | 57 +++++++++++++++ .../src/ServicesExternos.API/Dockerfile | 11 +++ tests/e2e/dashboard.spec.ts | 29 ++++++++ tests/e2e/leads.spec.ts | 30 ++++++++ tests/e2e/login.spec.ts | 28 +++++++ tests/playwright.config.ts | 15 ++++ 24 files changed, 967 insertions(+), 3 deletions(-) create mode 100644 .env.example create mode 100644 backend/src/Ventas.API/Dockerfile create mode 100644 docker-compose.yml create mode 100644 frontend/src/app/alumnos/page.tsx create mode 100644 frontend/src/app/arqueo/page.tsx create mode 100644 frontend/src/app/autorizador/page.tsx create mode 100644 frontend/src/app/contacto/page.tsx create mode 100644 frontend/src/app/cotizaciones/page.tsx create mode 100644 frontend/src/app/cursos/page.tsx create mode 100644 frontend/src/app/empresas/[id]/page.tsx create mode 100644 frontend/src/app/empresas/page.tsx create mode 100644 frontend/src/app/empresas/ventas/page.tsx create mode 100644 frontend/src/app/leads/[id]/page.tsx create mode 100644 frontend/src/app/leads/nuevo/page.tsx create mode 100644 frontend/src/app/leads/page.tsx create mode 100644 frontend/src/app/reportes/reimpresion/page.tsx create mode 100644 frontend/src/app/reportes/ventas-empresas/page.tsx create mode 100644 frontend/src/app/reportes/ventas/page.tsx create mode 100644 services-externos/src/ServicesExternos.API/Dockerfile create mode 100644 tests/e2e/dashboard.spec.ts create mode 100644 tests/e2e/leads.spec.ts create mode 100644 tests/e2e/login.spec.ts create mode 100644 tests/playwright.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8eae739 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# JWT +JWT_SECRET=generar-clave-segura-aqui +JWT_EXPIRATION=30 + +# LibreDTE +LIBREDTE_USER_HASH=ZDLimhVCDEXoHR6yDTJpb80ta7KG4DqI +LIBREDTE_AMBIENTE=0 + +# Transbank +TRANSBANK_API_KEY=tu-api-key +TRANSBANK_COMMERCE_CODE=tu-codigo-comercio +TRANSBANK_ENVIRONMENT=integration + +# SMTP +SMTP_PASSWORD=smith2251! diff --git a/ROADMAP.md b/ROADMAP.md index a8eb814..7e2cb69 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -88,11 +88,12 @@ - [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) +- [x] **3.5-3.8 Páginas** (22 páginas funcionales) - Leads (listado, detalle, nuevo) - Cotizaciones, Arqueo, Cursos, Alumnos - Empresas (listado, detalle, ventas) - - Reportes (3), Autorizador, Contacto + - Reportes (ventas, ventas-empresas, reimpresion) + - Autorizador, Contacto ### FASE 4: CONTENEDORES (~1 semana) @@ -123,7 +124,10 @@ | 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 en F1+F2 | Fase 3 | -| 2026-07-08 | F3 | Setup Next.js + Layout + Sidebar + Login + Auth + Dashboard + componentes + CSS | Resto páginas frontend | +| 2026-07-08 | F3 | Setup Next.js + Layout + Login + Auth + Dashboard + componentes + CSS | Resto páginas | +| 2026-07-08 | F3 | 22 páginas frontend completadas + Dockerfiles + docker-compose + .env | Fase 4 | +| 2026-07-08 | F4 | docker-compose.yml (3 servicios) + Dockerfiles + .env.example | Fase 5 | +| 2026-07-08 | F5 | Playwright E2E (7 escenarios) + playwright.config.ts | FIN | | | | | | | | | | | diff --git a/backend/src/Ventas.API/Dockerfile b/backend/src/Ventas.API/Dockerfile new file mode 100644 index 0000000..a3bc494 --- /dev/null +++ b/backend/src/Ventas.API/Dockerfile @@ -0,0 +1,11 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY . . +RUN dotnet restore Ventas.slnx +RUN dotnet publish src/Ventas.API/Ventas.API.csproj -c Release -o /app + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app . +EXPOSE 8080 +ENTRYPOINT ["dotnet", "Ventas.API.dll"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c292536 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +version: '3.8' +services: + backend-api: + build: + context: ./backend + dockerfile: src/Ventas.API/Dockerfile + environment: + - ConnectionStrings__Default=Host=192.168.0.254;Port=5432;Database=ichn;Username=postgres;Password=apoca11 + - Jwt__Secret=${JWT_SECRET} + - Jwt__Expiration=30 + ports: + - "5000:8080" + networks: + - ventas-network + extra_hosts: + - "host.docker.internal:192.168.0.254" + + services-externos: + build: + context: ./services-externos + dockerfile: src/ServicesExternos.API/Dockerfile + environment: + - LibreDTE__UserHash=${LIBREDTE_USER_HASH} + - LibreDTE__Ambiente=${LIBREDTE_AMBIENTE:-1} + - Transbank__ApiKey=${TRANSBANK_API_KEY} + - Transbank__CommerceCode=${TRANSBANK_COMMERCE_CODE} + - Transbank__Environment=${TRANSBANK_ENVIRONMENT:-integration} + - Email__Host=smtp.gmail.com + - Email__Port=587 + - Email__User=noresponder@norteamericano.cl + - Email__Password=${SMTP_PASSWORD} + ports: + - "5001:8080" + networks: + - ventas-network + + frontend: + build: ./frontend + environment: + - NEXT_PUBLIC_API_URL=http://backend-api:8080/api + - NEXT_PUBLIC_SERVICES_URL=http://services-externos:8080/api + ports: + - "3000:3000" + depends_on: + - backend-api + networks: + - ventas-network + +networks: + ventas-network: + driver: bridge diff --git a/frontend/src/app/alumnos/page.tsx b/frontend/src/app/alumnos/page.tsx new file mode 100644 index 0000000..ff7dba2 --- /dev/null +++ b/frontend/src/app/alumnos/page.tsx @@ -0,0 +1,67 @@ +'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'; + +export default function AlumnosPage() { + const { user, loading } = useAuth(); + const router = useRouter(); + const [alumnos, setAlumnos] = useState([]); + const [busqueda, setBusqueda] = useState(''); + const [loadingData, setLoadingData] = useState(true); + + useEffect(() => { + if (!loading && !user) router.push('/login'); + if (user) { + api.get('/alumno', { tipoBusqueda: 'NOMBRE', nombre: '' }).then(data => { + setAlumnos(Array.isArray(data) ? data : []); + setLoadingData(false); + }).catch(() => setLoadingData(false)); + } + }, [user, loading, router]); + + const handleSearch = async () => { + setLoadingData(true); + try { + const data = await api.get('/alumno', { tipoBusqueda: 'NOMBRE', nombre: busqueda }); + setAlumnos(Array.isArray(data) ? data : []); + } catch { /* ignore */ } + setLoadingData(false); + }; + + if (loading) return ; + + return ( +
+

Alumnos

+
+ setBusqueda(e.target.value)} + placeholder="Buscar alumno..." onKeyDown={e => e.key === 'Enter' && handleSearch()} /> + +
+
+
+ + + + + + {alumnos.map((a: any, i: number) => ( + + + + + + + ))} + {alumnos.length === 0 && } + +
RUTNombreMailTeléfono
{a.AlumnoRut || a.rut || a.Rut}{a.NombreAlumno || a.nombre || a.Nombre}{a.Email || a.mail || a.Mail}{a.Telefono || a.telefono || a.Fono}
Sin resultados
+
+
+
+ ); +} diff --git a/frontend/src/app/arqueo/page.tsx b/frontend/src/app/arqueo/page.tsx new file mode 100644 index 0000000..93082dc --- /dev/null +++ b/frontend/src/app/arqueo/page.tsx @@ -0,0 +1,54 @@ +'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'; + +export default function ArqueoPage() { + const { user, loading } = useAuth(); + const router = useRouter(); + const [ingresos, setIngresos] = useState([]); + const [loadingData, setLoadingData] = useState(true); + const fecha = new Date().toISOString().split('T')[0]; + + useEffect(() => { + if (!loading && !user) router.push('/login'); + if (user) { + api.get('/arqueo/todos', { fecha }).then(data => { + setIngresos(Array.isArray(data) ? data : []); + setLoadingData(false); + }).catch(() => setLoadingData(false)); + } + }, [user, loading, fecha, router]); + + if (loading || loadingData) return ; + + return ( +
+

Arqueo de Caja

+

Fecha: {new Date().toLocaleDateString()}

+
+
+ + + + + + {ingresos.map((r: any, i: number) => ( + + + + + + + ))} + {ingresos.length === 0 && } + +
CajeroCréditoDébitoTotal
{r.Cajero || r.cajero}${(r.Credito || r.credito || 0).toLocaleString()}${(r.Debito || r.debito || 0).toLocaleString()}${(r.Total || r.total || 0).toLocaleString()}
Sin movimientos hoy
+
+
+
+ ); +} diff --git a/frontend/src/app/autorizador/page.tsx b/frontend/src/app/autorizador/page.tsx new file mode 100644 index 0000000..c46310a --- /dev/null +++ b/frontend/src/app/autorizador/page.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useState } from 'react'; +import { api } from '@/services/api'; + +export default function AutorizadorPage() { + const [cotizacionId, setCotizacionId] = useState(''); + const [mensaje, setMensaje] = useState(''); + + const handleAutorizar = async () => { + try { + await api.put(`/cotizacion/${cotizacionId}/desactivar`); + setMensaje('Cotización autorizada'); + } catch { + setMensaje('Error al autorizar'); + } + }; + + return ( +
+

Autorizador de Descuentos

+
+
+ + setCotizacionId(e.target.value)} /> +
+ + {mensaje &&
{mensaje}
} +
+
+ ); +} diff --git a/frontend/src/app/contacto/page.tsx b/frontend/src/app/contacto/page.tsx new file mode 100644 index 0000000..517b788 --- /dev/null +++ b/frontend/src/app/contacto/page.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useState, FormEvent } from 'react'; +import { api } from '@/services/api'; + +export default function ContactoPage() { + const [mail, setMail] = useState(''); + const [mensaje, setMensaje] = useState(''); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + try { + await api.post('/email/send', { to: mail, subject: 'Contacto desde SAM', body: mensaje }); + alert('Correo enviado'); + setMail(''); + setMensaje(''); + } catch { + alert('Error al enviar'); + } + }; + + return ( +
+

Contacto

+
+
+
+ + setMail(e.target.value)} required /> +
+
+ +