From 5258974c7c06d09e3daa515b350aef1cc90475fe Mon Sep 17 00:00:00 2001 From: EstudiodeModaDev Date: Mon, 14 Sep 2026 09:08:10 -0500 Subject: [PATCH] Documentacion y re activacion --- docs/ARQUITECTURA.md | 163 ++++++++++++++++++ docs/modulos/README.md | 47 +++++ docs/modulos/app-raiz.md | 31 ++++ docs/modulos/auth.md | 42 +++++ docs/modulos/components-add-graph-users.md | 18 ++ docs/modulos/components-admin-settings.md | 25 +++ docs/modulos/components-admincells.md | 56 ++++++ .../modulos/components-agregar-colaborador.md | 24 +++ .../components-agregar-registro-vehicular.md | 18 ++ .../components-colaboradores-permanentes.md | 23 +++ .../components-detalles-colaborador.md | 14 ++ docs/modulos/components-mis-reservas.md | 22 +++ docs/modulos/components-modals.md | 20 +++ docs/modulos/components-notices.md | 21 +++ docs/modulos/components-permisos-app.md | 20 +++ docs/modulos/components-picoplaca.md | 25 +++ docs/modulos/components-registro-vehicular.md | 22 +++ docs/modulos/components-reporte-registros.md | 18 ++ docs/modulos/components-reportes.md | 27 +++ docs/modulos/components-reservar.md | 19 ++ docs/modulos/components-toast.md | 20 +++ docs/modulos/components-toggle-switch.md | 13 ++ docs/modulos/graph.md | 42 +++++ docs/modulos/hooks.md | 62 +++++++ docs/modulos/models.md | 34 ++++ docs/modulos/ports.md | 33 ++++ docs/modulos/services.md | 62 +++++++ docs/modulos/utils.md | 41 +++++ src/App.tsx | 7 +- src/Components/AdminCells/admin-cells.tsx | 17 +- src/Components/AdminCells/useAdminCells.ts | 3 + .../Notices/ReservationsDisabledNotice.tsx | 2 +- src/Hooks/useCeldas.ts | 27 ++- 33 files changed, 1007 insertions(+), 11 deletions(-) create mode 100644 docs/ARQUITECTURA.md create mode 100644 docs/modulos/README.md create mode 100644 docs/modulos/app-raiz.md create mode 100644 docs/modulos/auth.md create mode 100644 docs/modulos/components-add-graph-users.md create mode 100644 docs/modulos/components-admin-settings.md create mode 100644 docs/modulos/components-admincells.md create mode 100644 docs/modulos/components-agregar-colaborador.md create mode 100644 docs/modulos/components-agregar-registro-vehicular.md create mode 100644 docs/modulos/components-colaboradores-permanentes.md create mode 100644 docs/modulos/components-detalles-colaborador.md create mode 100644 docs/modulos/components-mis-reservas.md create mode 100644 docs/modulos/components-modals.md create mode 100644 docs/modulos/components-notices.md create mode 100644 docs/modulos/components-permisos-app.md create mode 100644 docs/modulos/components-picoplaca.md create mode 100644 docs/modulos/components-registro-vehicular.md create mode 100644 docs/modulos/components-reporte-registros.md create mode 100644 docs/modulos/components-reportes.md create mode 100644 docs/modulos/components-reservar.md create mode 100644 docs/modulos/components-toast.md create mode 100644 docs/modulos/components-toggle-switch.md create mode 100644 docs/modulos/graph.md create mode 100644 docs/modulos/hooks.md create mode 100644 docs/modulos/models.md create mode 100644 docs/modulos/ports.md create mode 100644 docs/modulos/services.md create mode 100644 docs/modulos/utils.md diff --git a/docs/ARQUITECTURA.md b/docs/ARQUITECTURA.md new file mode 100644 index 00000000..83e02909 --- /dev/null +++ b/docs/ARQUITECTURA.md @@ -0,0 +1,163 @@ +# Parking EDM — Arquitectura y Deuda Técnica + +> Documento generado a partir de una revisión completa del código fuente (`src/`) a fecha 2026-09-14, rama `main`. Complementa al [índice de módulos](./modulos/README.md), donde cada módulo (capa técnica o carpeta de componente) tiene su propio archivo `.md` con componentes, funciones y patrones de programación. + +## 1. ¿Qué es esta aplicación? + +**Parking EDM** es una SPA (Single Page Application) interna de Estudio de Moda para administrar el parqueadero de la compañía: reserva de celdas (parqueaderos), asignación fija de celdas a colaboradores, registro vehicular, control de "Pico y Placa", permisos de acceso a la app y reportería/ocupación. + +No existe backend propio: **Microsoft SharePoint Online actúa como base de datos** (listas) y **Microsoft Graph API** es la única capa de acceso a datos, consumida directamente desde el navegador con el token del usuario autenticado (Azure AD / MSAL). Es, en esencia, una aplicación *serverless* apoyada 100% en el tenant de Microsoft 365 de la empresa. + +## 2. Stack tecnológico + +| Capa | Tecnología | +|---|---| +| UI | React 19 + TypeScript, sin router (una sola vista con pestañas controladas por estado) | +| Build/dev | Vite 7, `@vitejs/plugin-react` | +| Estilo | CSS Modules (`*.module.css`) en la mayoría de componentes; estilos inline en algunos (ver deuda técnica) | +| Autenticación | `@azure/msal-browser` (MSAL) contra Azure AD, flujo *popup* | +| Datos | Microsoft Graph REST API v1.0, contra listas de SharePoint Online (no hay API propia ni base de datos SQL/NoSQL) | +| Export | `xlsx` (SheetJS) para exportar reportes a Excel | +| Lint | ESLint 9 + `typescript-eslint` + `eslint-plugin-react-hooks` | +| CI/CD | GitHub Actions → Azure Static Web Apps (despliegue estático del `dist/`) | + +No hay framework de testing configurado (no hay Jest/Vitest/Playwright, ni scripts de test en `package.json`). + +## 3. Arquitectura por capas + +La aplicación sigue, con intención, una arquitectura en capas inspirada en *repository pattern*: + +``` +Components (React) + │ usan + ▼ +Hooks (src/Hooks) ──────────────► estado de UI (loading/error/rows/paginación) + │ usan + ▼ +Services (src/Services) ───────► un "repositorio" por lista de SharePoint + │ (algunos, vía) + ▼ +Ports (src/Ports) ──────────────► adaptadores opcionales hacia forma de UI + │ usan + ▼ +GraphRest (src/graph/GraphRest.ts) ► wrapper fetch genérico para Graph API v1.0 + │ + ▼ +Microsoft Graph API → listas de SharePoint Online +``` + +En paralelo: +- **`src/auth`**: integración con MSAL/Azure AD, expone `useAuth()` (cuenta activa, `getToken()`, `signIn`/`signOut`). +- **`src/graph/GraphServicesContext.tsx`**: contexto React que instancia **un Service por lista** (usando `getToken` de `useAuth`) y los expone vía `useGraphServices()`. Es el punto central de *dependency injection* de la app. +- **`src/Models`**: tipos TypeScript — deberían ser solo contratos, pero en la práctica también contienen lógica (ver deuda técnica). +- **`src/utils`**: helpers puros (fechas, export a Excel, envío de correo, resolución de usuario, color de estado). + +### 3.1 Flujo de autenticación + +1. `main.tsx` monta `` (envolviendo toda la app) y dentro ``. +2. `AuthProvider` (`src/auth/AuthProvider.tsx`) inicializa MSAL (`initMSAL()`) sin forzar login automático; expone `{ready, account, getToken, signIn, signOut}`. +3. `App.tsx` muestra pantalla de login si no hay `account`; al autenticar (`msal.loginPopup`), monta `` → ``. +4. `GraphServicesProvider` crea un `GraphRest` con el `getToken` del contexto de auth y construye instancias de cada Service (parkingSlots, colaboradoresFijos, reservations, usuariosParking, settings, picoYPlaca, registroVeh, shared). +5. Todo *fetch* a Graph pasa por `GraphRest.call()`, que adjunta `Authorization: Bearer ` obtenido vía `acquireTokenSilent` (con *fallback* a popup si se requiere interacción). + +**Importante**: existe un segundo archivo de autenticación completo y no usado, `src/auth/authContext.tsx`, con su propia implementación de MSAL (flujo *redirect*, variables de entorno `VITE_AAD_*`, `logLevel`). No se importa desde ningún componente activo — es una implementación paralela abandonada (ver §5.1). + +### 3.2 Autorización (roles) + +No hay un sistema de roles de Azure AD ni App Roles: el rol de usuario (`admin` / `usuario`) se guarda como un campo de texto (`Rol`) en la lista SharePoint **UsuariosParking**, indexada por email en el campo `Title` (convención implícita, no documentada en el modelo). `SharedServices.getRole(email)` hace la consulta y, ante cualquier error, **retorna `null` silenciosamente** — un fallo de red se interpreta igual que "sin rol", sin registro (log) de la causa real. Cualquier usuario autenticado con permiso `Permitidos=true` puede however cambiar su propio rol vía un botón "Cambiar rol" en la cabecera (`App.tsx`), lo cual es una decisión de producto a validar con negocio (el control de acceso depende de un campo editable por Graph API con los mismos permisos delegados que el resto de la app). + +### 3.3 Persistencia (listas de SharePoint) + +El sitio de SharePoint (`estudiodemoda.sharepoint.com/sites/TransformacionDigital/IN/SA`) actúa como base de datos, con estas listas activas: + +| Lista (display name) | Service | Propósito | +|---|---|---| +| `parkingslots` | `ParkingSlotsService` | Celdas de parqueo (Tipo Carro/Moto, Activa, Itinerancia) | +| `Colaboradores fijos` | `ColaboradoresFijosService` | Colaboradores con celda fija asignada | +| `reservations` | `ReservationsService` | Reservas de celda (fecha, turno, código, estado) | +| `usuariosparking` | `UsuariosParkingService` | Rol/permiso de cada usuario de la app | +| `settings` | `SettingsService` | Configuración global (horarios, T&C, días visibles, flag Pico y Placa) — un único registro fijo | +| `pico y placa` | `PicoYPlacaService` | Reglas de restricción vehicular por día/dígito | +| `RegistroVehicular` | `RegistroVehicularService` | Registro de vehículos reportados/inscritos | + +Además, el grupo de Microsoft 365/Entra ID con GUID hardcodeado `79012669-3208-412c-bae2-97d79f5f5f15` (en `usuariosApp.tsx`) controla quién tiene "acceso a la app" a nivel de directorio (independiente de la lista `usuariosparking`) — dos mecanismos de control de acceso distintos y no sincronizados explícitamente entre sí. + +Todos los ids de sitio/lista se resuelven una vez y se cachean en `localStorage` bajo la clave `sp:{hostname}{sitePath}:{listName}` (patrón "ensureIds", repetido en 7-8 servicios). + +### 3.4 Despliegue + +Dos workflows de GitHub Actions (`azure-static-web-apps-proud-mushroom-*.yml` y `...-purple-mushroom-*.yml`) despliegan **el mismo build** (`dist/`) a **dos Azure Static Web Apps distintas** en cada push a `main` (y en cada PR). No hay separación de entornos (staging/producción), ni paso de `lint`/`build` de verificación previo al deploy más allá del propio build de SWA, ni ejecución de tests (no existen). + +## 4. Panorama de tamaño por capa (líneas de código) + +| Carpeta | LOC aprox. | Nota | +|---|---|---| +| `src/Components/AdminCells` | 1801 | el módulo más grande, con mucho margen; contiene el componente `slotDetailsModal.tsx` (955 líneas) | +| `src/Hooks` | 2394 | 14 archivos, con duplicación estructural significativa | +| `src/Services` | 1683 | 13 archivos, alto nivel de código repetido (patrón `ensureIds`) | +| `src/Models` | 625 | 20 archivos, mezcla tipos + lógica + un hook duplicado | +| `src/auth` | 294 | dos implementaciones paralelas (una muerta) | +| `src/graph` | 226 | `GraphRest` + `GraphServicesContext` (DI) | +| `src/utils` | 213 | helpers puros, bajo nivel de deuda | +| `src/Ports` | 126 | solo cubre 2 de ~11 servicios | + +## 5. Deuda técnica + +Organizada por severidad/impacto. Cada punto fue verificado leyendo el código fuente completo de los archivos involucrados (no se listan sospechas sin evidencia). + +### 5.1 Crítica — riesgos de seguridad y bugs funcionales activos + +1. **Archivo `.env` versionado en git** (`git ls-files` lo confirma) con `VITE_AZURE_CLIENT_ID`, `VITE_AZURE_TENANT_ID`, `VITE_AZURE_REDIRECT_URI`, pese a que `.gitignore` excluye `.env` — fue forzado al repositorio en algún momento anterior. Estas variables, además, **no las usa el flujo de auth activo** (`src/auth/msal.ts` hardcodea el `clientId`/`authority` directamente en el código); solo las lee la implementación muerta `authContext.tsx`. Recomendación: purgar el archivo del historial si contiene algo sensible más allá de un Client ID público, y decidir una única fuente de verdad para la config de MSAL. +2. **HTML sin sanitizar renderizado con `dangerouslySetInnerHTML`**: el campo "Términos y Condiciones" se edita como texto libre en `AdminSettings.tsx` (sin sanitización al guardar) y se inyecta tal cual en el DOM desde `Components/Modals/modals.tsx` (sin sanitización al leer). Cualquier cuenta admin comprometida podría inyectar HTML/script ejecutado en el navegador de todo usuario que intente reservar. +3. **Condición de carrera en la creación de reservas** (`useReservar.ts`): el flujo es "verificar disponibilidad → crear" sin bloqueo/transacción; dos usuarios pueden reservar simultáneamente la misma celda/turno. El campo `Codigo` (consecutivo legible) se calcula contando **todas** las reservas existentes (`getAll({top: 20000})`) e incrementando — no es atómico y puede duplicarse bajo concurrencia. Hay además un segundo `+1` redundante en `ReservationsService.reservationCode`, generando un desfase adicional en la numeración. +4. **`RegistroVehicularService.create`/`update` están tipados contra el modelo equivocado** (`Colaboradoresfijos` en vez de `RegistroVehicularSP`) — TypeScript no puede detectar campos mal escritos en tiempo de compilación; es un bug de tipos con impacto real en runtime si los campos no coinciden. +5. **La regla de negocio de "Pico y Placa" no se aplica en ningún punto de la reserva.** La pantalla admin permite configurar qué dígitos de placa están restringidos por día, pero `useReservar.ts` nunca consulta esa lista — la restricción vehicular es puramente decorativa hoy. +6. **`ReservationsDisabledNotice`'s prop `compact` es un no-op** (`{compact ? '.' : '.'}` — ambas ramas retornan lo mismo) pese a que dos pantallas (`AdminCells`, `slotDetailsModal`) lo pasan esperando un render distinto. +7. **`useAdminCells.ts` invoca hooks de React de forma condicional** (`slotsSvc ? useCeldas(slotsSvc) : {...}`), violando las Reglas de los Hooks. Funciona hoy porque `ready` se estabiliza rápido, pero es una fuente latente de comportamiento indefinido si el ciclo de auth cambia. + +### 5.2 Alta — duplicación estructural y decisiones de arquitectura + +8. **Patrón `ensureIds`/caché/escape de OData duplicado literalmente en 7-8 servicios** (`Colaboradoresfijos`, `Log` (ver #9), `ParkingSlot`, `PicoPlaca`, `RegistroVehicular`, `Reservations`, `Setting`, `UsuariosParking`), sin una clase base compartida (`SharePointListRepository`). Las copias ya divergieron entre sí (p. ej. `ParkingSlot.service.ts` agrega dos-puntos finales al *site path* que ninguno de los demás usa). +9. **`src/Services/Log.service.ts` es un duplicado byte-a-byte de `Colaboradoresfijos.service.ts`** (mismo hash MD5), exporta la misma clase `ColaboradoresFijosService`, y no tiene ninguna funcionalidad de logging pese a su nombre. No se usa en ningún lado — código muerto que además podría causar colisión de nombres de clase si algún día se importa. +10. **Tres a cuatro implementaciones independientes de cliente HTTP para Graph** coexisten junto al wrapper "oficial" `GraphRest`: `Services/GraphUsers.service.ts`, `Services/GruposCorreo.service.ts`, `Hooks/GraphUsers.ts` (casi idéntico al anterior) y un acceso directo con `as any` a un método privado de `GraphRest` en `User.Service.ts` para descargar la foto de perfil. La lógica de membresía de grupos de Azure AD está duplicada en al menos dos archivos que no se reutilizan entre sí. +11. **`Ports/` no es una capa de arquitectura consistente**: solo 2 de ~11 servicios (`PicoYPlacaService`, `SettingsService`) tienen un adaptador `*.port.ts`; el resto se consume directo desde componentes/hooks vía `useGraphServices()`. Además, `Ports/settingsPort.ts` construye un payload con un campo `TyC` que el modelo real de `SettingsService` nunca lee (usa `as any` para evitar el error de tipos) — el valor se escribe pero nunca se recupera, un bug de integridad de datos silenciado por el cast. +12. **Componente "todo en uno" de 955 líneas**: `Components/AdminCells/slotDetailsModal.tsx` maneja 4 flujos distintos (asignar/desasignar celda fija, reservar, editar celda, eliminar celda), instanciando sus propios servicios de Graph con constantes hardcodeadas (duplicando lo que ya expone `useGraphServices()`), con estilos 100% inline y arrays de dependencias de `useCallback` ajustados a mano e incorrectos. +13. **Módulo de creación de reservas duplicado, uno de los dos muerto**: `Components/Reservar/Reservar.tsx` (exportado como `Availability`) implementa un flujo completo de reserva pero **no está montado en ningún punto de `App.tsx`** — la UI real de reserva vive dentro de `AdminCells`/`slotDetailsModal.tsx`, que sí respeta el flag `RESERVATIONS_DISABLED` (cosa que `Reservar.tsx` ignora si algún día se remonta). +14. **Tres modales de "elegir colaborador del directorio + prellenar + validar + guardar" casi idénticos** (`ModalAgregarColaborador`, `ModalAgregarPermiso`, `ModalAgregarRegistro`) sin componente compartido — y aplicando la regla de "nombre genérico no permitido" (`nameProve`) de forma inconsistente (2 de 4 puntos de selección de trabajador la aplican, 2 no). +15. **Tres pantallas de "lista + filtro segmentado + búsqueda + tabla + paginación" casi idénticas** (`Colaboradores.tsx`, `usuariosApp.tsx`, `RegistroVehicular.tsx`) con la misma estructura pero ya divergentes (opciones de tamaño de página distintas: `[5]`, `[5]`, `[5,10,20]`). +16. **Modelos y Hooks con capas cruzadas**: `Models/Settings.ts` importa un tipo desde `Services/Setting.service.ts`, invirtiendo la dirección de dependencia esperada (Models no debería depender de Services). `Models/useCeldas.ts` es en realidad un **hook de React completo** (no un tipo), duplicado y desactualizado respecto a `Hooks/useCeldas.ts` real — vive en la carpeta equivocada y no se usa, pero podría importarse por error dado que comparte nombre con el hook real. + +### 5.3 Media — inconsistencias y código muerto + +17. **Archivos con nombre que no corresponde a su contenido**: `Hooks/usePicoPlaca.ts` en realidad exporta `useRecipients` (nada que ver con Pico y Placa — la lógica real de esa regla de negocio vive, sin usarse desde la pantalla que la necesita, en `Hooks/utils.ts`); `Hooks/GraphUsers.ts` exporta `useGroupMembers`; `Services/GraphUsers.service.ts` trae en su propio comentario de cabecera el nombre `GroupMembers.service.ts`. +18. **Dos hooks distintos llamados `useReporteria`** (`Hooks/reporteRegistro.ts` y `Hooks/useReportes.ts`), con estructura similar pero fuentes de datos distintas — solo se distinguen por el alias de import. +19. **`Components/ToggleSwitch/ToggleSwitch.tsx`** es la única implementación accesible (ARIA, teclado) de un switch en toda la app, y **no se usa en ningún lado** — mientras que la funcionalidad `toggleEstado` de `useCeldas` (activar/desactivar celda) existe en el hook pero nunca se expone en la UI de `AdminCells`. +20. **Paginación "decorativa" en `Mis-Reservas`**: los controles de página (`pageIndex`/`pageSize`) existen y calculan `hasNext`, pero la tabla siempre renderiza todas las filas obtenidas — cambiar de página dispara un refetch completo al servidor sin necesidad. +21. **`ToastProvider` cierra el toast al pasar el mouse por encima** (`onMouseEnter={() => remove(t.id)}`), el comportamiento inverso al patrón habitual de "pausar al pasar el mouse". +22. **Nombres de campo inconsistentes para el mismo concepto** entre modelos de distintas listas: "tipo de vehículo" aparece como `TipoCelda`, `TipoVeh`, `Tipodevehiculo` y `VehicleType` según el archivo; el campo `ID` es `string` en un modelo y `number` en otro, forzando *casts* defensivos en cada hook consumidor. +23. **`Models/UsuariosParking.ts` modela ~30 campos** (aparentemente copiados de un esquema autogenerado de Power Automate/Power Platform) **de los cuales el servicio solo usa 4** (`ID, Title, Rol, Permitidos`) — ruido significativo en el tipo. +24. Múltiples **mapeadores duplicados** para la misma conversión "reserva cruda → fila de UI" (`Reservation.ts::mapReservationToUI` vs. un `mapModelToUI` independiente dentro de `useMisReservas.ts`, y un tercer modelo de vista `Reportes.ts::ReservaUI` con nombres de campo distintos para el mismo dato). +25. Uso extendido de **`any`**: prácticamente todo `toModel(item: any)`, casts `as any` en payloads de creación/actualización, y varios `catch (e: any)` — no hay validación de esquema en tiempo de ejecución entre la respuesta cruda de Graph y el modelo tipado. + +### 5.4 Baja — estilo, limpieza y comentarios muertos + +26. Bloques comentados de código dejado en producción: implementación alterna de envío de correo en `RegistroVehicular.tsx` (con un typo `;2`), autocompletado de correo comentado en `ModalAgregarRegistro.tsx`. +27. `console.log`/`console.table`/`console.warn` de depuración en varios hooks de producción (`useReservar.ts`, `useMisReservas.ts`, `Reservations.service.ts`). +28. Props no usadas ("dead props"): `slots`/`slotsLoading` en `ModalAgregarColaborador`/`ModalAgregarPermiso`; botón para abrir `ModalVerColaborador` comentado en `Colaboradores.tsx` (el modal queda inalcanzable en producción). +29. Estilo inconsistente: mezcla de CSS Modules, clases globales sueltas y objetos de estilo inline (`slotDetailsModal.tsx`, `PicoPlaca.tsx`) dentro del mismo conjunto de pantallas. +30. Mensajes al usuario vía `alert()`/`window.confirm()` en vez del `ToastProvider` ya existente en la app (`admin-cells.tsx`, `slotDetailsModal.tsx`, `PicoPlaca.tsx`, confirmaciones de borrado en varias pantallas). +31. Sin capa de i18n: todos los textos están hardcodeados en español directamente en JSX — aceptable para una herramienta interna de un solo idioma, pero a tener en cuenta si se planea extender a otros países/idiomas. +32. Dos workflows de despliegue duplicados en `.github/workflows/` sin diferenciación de entorno. + +## 6. Recomendaciones priorizadas + +1. **Cerrar el hueco de seguridad de `dangerouslySetInnerHTML`** (sanitizar en guardado o en render con una librería como DOMPurify) y decidir qué hacer con el `.env` versionado. +2. **Arreglar la condición de carrera de reservas**: mover la generación de `Codigo` y la verificación de disponibilidad a una operación atómica del lado de Graph/SharePoint (o aceptar el riesgo documentándolo) antes de reactivar la creación de reservas. +3. **Extraer una clase base `SharePointListRepository`** para eliminar la duplicación de `ensureIds`/caché/escape en los 7+ servicios, y eliminar `Log.service.ts` y `Models/useCeldas.ts` (código muerto confirmado). +4. **Decidir una única implementación de autenticación** (eliminar `src/auth/authContext.tsx` si no se va a usar) y una única fuente de configuración de MSAL (env vars *o* hardcode, no ambas). +5. **Consolidar el patrón repetido "lista + filtro + búsqueda + paginación"** en un hook/componente compartido, y el patrón "elegir colaborador del directorio" en un componente reutilizable — reduciría ~3 archivos a 1 en cada caso y evitaría que seguir divergiendo. +6. **Decidir el futuro de `Reservar.tsx`** (código muerto) y de la regla de "Pico y Placa" (implementarla en `useReservar.ts` o remover la pantalla de configuración si no se va a aplicar). +7. Antes de cualquier refactor grande, **introducir un mínimo de pruebas automatizadas** (no existe ninguna hoy) sobre todo en `useReservar.ts` y los servicios con lógica de negocio (`PicoPlaca.ts`, `Reservations.service.ts`). + +--- +Ver el [índice de módulos](./modulos/README.md) para el detalle módulo por módulo (un `.md` por cada capa técnica y por cada carpeta de componente), con firmas, comportamiento y hallazgos puntuales por archivo. diff --git a/docs/modulos/README.md b/docs/modulos/README.md new file mode 100644 index 00000000..4d339b9d --- /dev/null +++ b/docs/modulos/README.md @@ -0,0 +1,47 @@ +# Índice de módulos — Parking EDM + +> Documentación técnica por módulo. Ver primero [../ARQUITECTURA.md](../ARQUITECTURA.md) para la visión general (stack, capas, flujo de auth, deuda técnica priorizada). + +## Núcleo de la aplicación + +| Módulo | Descripción | +|---|---| +| [Arranque de la app](./app-raiz.md) | `App.tsx`, `main.tsx` — composición raíz, pestañas, sesión y rol | +| [Autenticación](./auth.md) | `src/auth` — MSAL/Azure AD (implementación activa + implementación paralela sin usar) | +| [Capa Graph](./graph.md) | `src/graph` — `GraphRest` (cliente HTTP) y `GraphServicesContext` (DI de servicios) | +| [Servicios](./services.md) | `src/Services` — un repositorio por lista de SharePoint | +| [Ports](./ports.md) | `src/Ports` — adaptadores hacia forma de UI (solo 2 de ~11 servicios) | +| [Hooks](./hooks.md) | `src/Hooks` — estado de UI (loading/error/paginación) sobre los servicios | +| [Models](./models.md) | `src/Models` — tipos de dominio, modelos crudos de SharePoint y modelos de vista | +| [Utils](./utils.md) | `src/utils` — helpers puros (fechas, Excel, correo, color de estado) | + +## Componentes — Administración + +| Módulo | Descripción | +|---|---| +| [AdminCells](./components-admincells.md) | Pestaña "Celdas" — dashboard de capacidad, reserva rápida, detalle/asignación de celda (módulo más grande del proyecto) | +| [Admin-Settings](./components-admin-settings.md) | Pestaña "Administración" — días visibles, horarios, Términos y Condiciones | +| [Colaboradores-Permanentes](./components-colaboradores-permanentes.md) | Sub-tab "Colaboradores Fijos" | +| [AgregarColaborador](./components-agregar-colaborador.md) | Modal de alta de colaborador fijo | +| [DetallesColaborador](./components-detalles-colaborador.md) | Modal de solo lectura (hoy inalcanzable en producción) | +| [PermisosApp](./components-permisos-app.md) | Sub-tab "Usuarios APP" — membresía del grupo de acceso a la app | +| [AddGraphUsers](./components-add-graph-users.md) | Modal para otorgar acceso a la app | +| [RegistroVehicular](./components-registro-vehicular.md) | Sub-tab "Registro vehicular" | +| [AgregarRegistroVehicular](./components-agregar-registro-vehicular.md) | Modal de alta de registro vehicular | +| [Modals](./components-modals.md) | Modal de confirmación genérico (con Términos y Condiciones opcionales) | +| [ToggleSwitch](./components-toggle-switch.md) | Switch accesible genérico (sin uso actualmente) | + +## Componentes — Reservas, Pico y Placa, Reportes y UI compartida + +| Módulo | Descripción | +|---|---| +| [Mis-Reservas](./components-mis-reservas.md) | Tabla de reservas propias (o de todos, en modo admin) | +| [Reservar](./components-reservar.md) | Pantalla de creación de reserva — **no montada actualmente** | +| [PicoPlaca](./components-picoplaca.md) | Pestaña "Pico y placa" — reglas de restricción vehicular (hoy no aplicadas en la reserva) | +| [ReporteRegistros](./components-reporte-registros.md) | Reporte de registro vehicular | +| [Reportes](./components-reportes.md) | Pestaña "Reportes" (contenedor) — aforo de celdas + reporte de registro | +| [Notices](./components-notices.md) | Aviso y *feature flag* de reservas deshabilitadas | +| [Toast](./components-toast.md) | Sistema de notificaciones (toasts) de toda la app | + +--- +Para el resumen priorizado de deuda técnica (con severidad crítica/alta/media/baja) y recomendaciones, ver [../ARQUITECTURA.md §5](../ARQUITECTURA.md#5-deuda-técnica). diff --git a/docs/modulos/app-raiz.md b/docs/modulos/app-raiz.md new file mode 100644 index 00000000..cc126e62 --- /dev/null +++ b/docs/modulos/app-raiz.md @@ -0,0 +1,31 @@ +# Módulo: Arranque de la app (`src/App.tsx`, `src/main.tsx`) + +> Ver también: [Arquitectura general](../ARQUITECTURA.md) · [Índice de módulos](./README.md) + +## `src/main.tsx` +Punto de entrada. Monta React (`StrictMode`) envolviendo `` en `` (`src/auth/AuthProvider.tsx`). No hay router: toda la navegación de la app es estado interno de `App.tsx`. + +## `src/App.tsx` +Componente raíz de la aplicación. Renderiza tres estados posibles según `useAuth()`: +- `!ready` → pantalla "Conectando…". +- `!account` → pantalla de login con botón "Iniciar sesión" (`signIn()` → `msal.loginPopup`). +- con sesión → monta ``. + +### `AppInner` (función interna, no exportada) +- Carga el perfil (`UserService.getMeBasic()`) y el rol/permiso (`SharedServices.getRole(mail)`) en dos `useEffect` secuenciales (primero perfil, luego rol dependiente del perfil). +- `NAVS_ADMIN` (línea 24) define las pestañas visibles para administradores: Reservas, Celdas, Administración, Pico y placa, Colaboradores, Reportes — solo visibles si `userRole === 'admin'`. +- Usuarios no-admin solo ven `ReservationsDisabledNotice` + `MisReservas` (modo de solo lectura de sus propias reservas). +- `useRoleHelpers()` (función local) implementa `changeUser(email)`: alterna el campo `Rol` en la lista `usuariosparking` entre `admin`/`Usuario` — es el mecanismo detrás del botón "Cambiar rol" en la cabecera (`HeaderBar`), visible solo si `Permitidos=true` para ese usuario. +- Envuelve todo el contenido autenticado en `` para exponer `useToast()` a los descendientes. + +### `HeaderBar` (función local, reutilizable) +Cabecera común a todas las pantallas (logueado o no): muestra avatar/nombre/correo/rol, botón "Cambiar rol" (si aplica) y la acción primaria (Iniciar/Cerrar sesión). + +## Patrones de programación +- Composición por render condicional de pestañas (no hay `react-router`); el estado de sesión/rol vive en este único componente y se pasa hacia abajo por props, no por un contexto adicional. +- `useMemo` para instanciar servicios derivados de `useGraphServices()` (`UserService`, `SharedServices`) una sola vez por cambio de `graph`/`usuariosParking`. +- Flags booleanos independientes por cada etapa asíncrona (`userLoading`, `permLoading`, `changingRole`) en vez de una máquina de estados única. + +## Notas de calidad +- La normalización de la respuesta de Graph (`normRows`, dentro de `useRoleHelpers`) contempla varias formas (`res?.data`, `res?.value`, array plano) — indicio de que el contrato de retorno de los Services no es 100% uniforme entre sí (ver [Servicios](./services.md)). +- El cambio de rol (`changeUser`) reescribe directamente el campo `Rol` vía Graph con los mismos permisos delegados que el resto de la app — no hay una capa de autorización adicional del lado servidor; ver deuda técnica de autorización en [ARQUITECTURA.md §3.2](../ARQUITECTURA.md#32-autorización-roles). diff --git a/docs/modulos/auth.md b/docs/modulos/auth.md new file mode 100644 index 00000000..9cc2075c --- /dev/null +++ b/docs/modulos/auth.md @@ -0,0 +1,42 @@ +# Módulo: Autenticación (`src/auth`) + +> Ver también: [Arquitectura general](../ARQUITECTURA.md) · [Índice de módulos](./README.md) + +Este módulo integra la app con Azure AD (Entra ID) vía MSAL (`@azure/msal-browser`). Contiene **dos implementaciones paralelas**; solo una está activa. + +## `src/auth/msal.ts` — implementación activa (bajo nivel) +Instancia única de `PublicClientApplication` con `clientId`/`authority` **hardcodeados** directamente en el código (no usa variables de entorno). `cache.cacheLocation = 'localStorage'`. + +`SCOPES` solicitados: `openid, profile, email, User.Read, Sites.ReadWrite.All, Directory.Read.All, Mail.Send` (con `Group.ReadWrite.All` dejado comentado). + +Funciones exportadas: +- `initMSAL()`: inicializa MSAL una sola vez (guard `initialized`). +- `ensureActiveAccount()`: garantiza que siempre haya una "active account" seleccionada en MSAL. +- `ensureLogin(): Promise`: login interactivo vía **popup** (`msal.loginPopup`). +- `getAccessToken(): Promise`: intenta `acquireTokenSilent`; si requiere interacción (`InteractionRequiredAuthError`), hace *fallback* a `acquireTokenPopup`. +- `logout()`: `msal.logoutPopup`. + +## `src/auth/AuthProvider.tsx` — implementación activa (contexto React) +Contexto React que envuelve `msal.ts` y es el que efectivamente consume el resto de la app vía `useAuth()`: +```ts +type AuthCtx = { ready, account, getToken, signIn, signOut }; +``` +No fuerza login automático al montar — solo inicializa MSAL (`initMSAL()`) y deja `ready = true`; el login ocurre cuando el usuario pulsa "Iniciar sesión" en `App.tsx`. + +## `src/auth/authContext.tsx` — **implementación paralela sin usar (código muerto)** +Segundo archivo con una implementación de auth completa e independiente: +- Instancia propia de `PublicClientApplication`. +- Lee configuración desde variables de entorno `import.meta.env.VITE_AAD_TENANT_ID` / `VITE_AAD_CLIENT_ID` / `VITE_AAD_REDIRECT_URI` (que sí existen en el `.env` versionado del repo, pero no las usa nadie más). +- Usa flujo de **redirect** (`loginRedirect`/`acquireTokenRedirect`) en vez de popup. +- Define su propio `useAuth()`/`AuthProvider` con la **misma firma pública** (`ready, account, getToken`) que la versión activa, más `logout` en vez de `signIn`/`signOut`. + +Ningún componente de la app importa este archivo — se verificó por búsqueda en todo `src/`. Es la implementación "descartada" de un rediseño de auth que no se completó ni se limpió. + +## Patrones de programación +- Context + hook de consumo (`useAuth()`) con guard de error si se usa fuera del provider — patrón estándar repetido en otros contextos de la app (`GraphServicesContext`, `ToastProvider`). +- `useMemo`/`useCallback` para mantener estable la identidad de `value` del contexto entre renders. + +## Notas de calidad (deuda técnica) +- Tener dos implementaciones de auth completas y con la misma interfaz pública es una fuente de confusión para cualquier persona nueva en el proyecto: hay que verificar cuál se usa realmente (`AuthProvider.tsx`, importado desde `main.tsx`) antes de tocar código de sesión. +- El archivo `.env` versionado en git alimenta únicamente a la implementación muerta (`authContext.tsx`) — ver [ARQUITECTURA.md §5.1, punto 1](../ARQUITECTURA.md#51-crítica--riesgos-de-seguridad-y-bugs-funcionales-activos). +- Recomendación: eliminar `authContext.tsx` si no hay planes de migrar a flujo *redirect*, o documentar por qué se mantiene. diff --git a/docs/modulos/components-add-graph-users.md b/docs/modulos/components-add-graph-users.md new file mode 100644 index 00000000..5b79b076 --- /dev/null +++ b/docs/modulos/components-add-graph-users.md @@ -0,0 +1,18 @@ +# Módulo: `Components/AddGraphUsers` — Otorgar acceso a la app + +> Ver también: [Arquitectura general](../ARQUITECTURA.md) · [Índice de módulos](./README.md) · [Usuarios de la app](./components-permisos-app.md) + +Modal invocado desde [`usuariosApp.tsx`](./components-permisos-app.md) para elegir un usuario del directorio y otorgarle acceso a la app (agregarlo al grupo de Microsoft 365 correspondiente). + +## `ModalAgregarPermiso.tsx` (196 líneas) +`Props = {isOpen, onClose, onSave?, slotsLoading?, workers: any[], workersLoading?}` — solo devuelve al padre `{userId, name, mail}`; la escritura real en el grupo la hace el componente padre. + +- Clon estructural casi idéntico a [`ModalAgregarColaborador`](./components-agregar-colaborador.md) (mismo patrón de combobox, efectos, botones de pie). +- Nombre/Correo son **de solo lectura** (`readOnly`) — solo se llenan eligiendo del directorio, a diferencia del modal de colaboradores donde siguen editables tras el prellenado. +- Valida además que se haya elegido explícitamente un `selectedUserId` (no basta con nombre/correo no vacíos) antes de habilitar "Guardar". +- **No** llama `nameProve()` — a diferencia de `ModalAgregarColaborador` y del selector de reserva de `slotDetailsModal.tsx`, esta pantalla no filtra cuentas genéricas/de rol al otorgar acceso. + +## Notas de calidad (deuda técnica) +- `workers: any[]` (línea 11) — el único punto del proyecto donde el directorio de trabajadores no se tipa como `Worker[]`. +- `slotsLoading?: boolean` es una prop irrelevante para este dominio (otorgar acceso no tiene nada que ver con celdas de parqueo) y tampoco se usa en el cuerpo — residuo de copiar el modal de colaboradores como plantilla. +- Ausencia de `nameProve()` aquí, presente en otros dos puntos de selección de trabajador, es una inconsistencia real de la regla de negocio "no permitir cuentas genéricas": un admin podría otorgar acceso de app a una cuenta como `"facturacion"` aunque la misma elección sería rechazada en el alta de colaboradores. diff --git a/docs/modulos/components-admin-settings.md b/docs/modulos/components-admin-settings.md new file mode 100644 index 00000000..642baf28 --- /dev/null +++ b/docs/modulos/components-admin-settings.md @@ -0,0 +1,25 @@ +# Módulo: `Components/Admin-Settings` — Configuración global + +> Ver también: [Arquitectura general](../ARQUITECTURA.md) · [Índice de módulos](./README.md) · [Servicios](./services.md) · [Ports](./ports.md) + +Pestaña "Administración" (`selected === 'admin'` en `App.tsx`). + +## `AdminSettings.tsx` (307 líneas) +`Props = {settingsSvc: SettingsService, settingsItemId?: string (default '1')}`. + +Formulario de configuración global: número de días visibles para reservar (`VisibleDays`), Términos y Condiciones (textarea HTML libre), y horarios AM/PM (`InicioManana/FinalManana/InicioTarde/FinalTarde`, expuestos en el `FormState` con el nombre interno de SharePoint `InicioHorarioMa_x00f1_ana`/`FinalMa_x00f1_ana` — artefacto de codificación de "Mañana" que SharePoint genera cuando no puede usar tildes en el nombre interno de columna). + +- Convierte valores de hora heterogéneos (numérico o texto tipo `"7:00 a. m."`) mediante un parser propio `fromHH` (líneas 29-59): quita espacios NBSP, normaliza variantes de `a.m./p.m.`, y cae a interpretación de 24 horas si no reconoce el formato. +- Renderiza inputs `` para las 4 horas, convirtiendo ida y vuelta con `toHH`/`fromHH` en cada cambio. +- Validación cliente: `horariosInvalid` exige `InicioAM < FinalAM` y `InicioTarde < FinalTarde`; deshabilita "Guardar" si no se cumple, con mensaje de advertencia inline. +- `save()` mapea el formulario de vuelta al campo real `TerminosyCondiciones` y llama `settingsSvc.update(...)`. + +## Patrones de programación +- Helpers puros de parseo/formateo (`clamp`, `clampHour`, `toHH`, `fromHH`) definidos a nivel de módulo (no dentro de handlers JSX) — testeables de forma aislada, aunque por su naturaleza (parsing de convenciones de SharePoint) encajarían mejor en `Models/time.ts` o un util dedicado. +- Componente pequeño, de una sola responsabilidad — uno de los más contenidos del proyecto. + +## Notas de calidad (deuda técnica) +- `row: any` al leer el ítem cargado desde el servicio (línea 85) — sin tipo real pese a que `SettingsService` sí está tipado. +- El artefacto de SharePoint `Ma_x00f1_ana` se filtra directamente hasta el tipo `FormState` y a los `id` de JSX, en vez de quedar aislado detrás de una capa de mapeo — ver también [Models — `Settings.ts`](./models.md). +- El campo `TyC` (Términos y Condiciones) se guarda **sin sanitizar**, y se renderiza después vía `dangerouslySetInnerHTML` en [`Components/Modals/modals.tsx`](./components-modals.md) — riesgo de XSS almacenado si una cuenta admin se ve comprometida. Ver [ARQUITECTURA.md §5.1, punto 2](../ARQUITECTURA.md#51-crítica--riesgos-de-seguridad-y-bugs-funcionales-activos). +- Sin capa de i18n (textos y tooltips hardcodeados en español), consistente con el resto de la app. diff --git a/docs/modulos/components-admincells.md b/docs/modulos/components-admincells.md new file mode 100644 index 00000000..bbd79b92 --- /dev/null +++ b/docs/modulos/components-admincells.md @@ -0,0 +1,56 @@ +# Módulo: `Components/AdminCells` — Administración de celdas + +> Ver también: [Arquitectura general](../ARQUITECTURA.md) · [Índice de módulos](./README.md) · [Hooks](./hooks.md) · [Servicios](./services.md) + +Pestaña "Celdas" (`selected === 'celdas'` en `App.tsx`). Es el módulo más grande del proyecto (1801 líneas en total) y el que concentra más deuda técnica de UI. + +## `admin-cells.tsx` (634 líneas) — pantalla principal +Dashboard de capacidad ("Capacidad hoy": disponibles Carro/Moto, % de aforo, turno actual), formulario de "Reserva rápida" (combobox tipo *typeahead* sobre el directorio `workers`, filtrado a 10 resultados en cliente, gateado por `RESERVATIONS_DISABLED`), filtros (Tipo Carro/Moto, Tipo de usuario/Itinerancia, **Estado** Activa/Inactiva/Todas — adición reciente sin commitear al momento de esta revisión, búsqueda por código con Enter, selector de tamaño de página), grilla de tarjetas con badges AM/PM de ocupación (`renderTurnBadge`), paginación y modal de creación de celda. + +- No tiene props (`React.FC`); combina el estado de `useAdminCells()` con ~10 variables de estado propias para el formulario de reserva rápida (`qrDate, qrTurn, qrVehicle, qrUserEmail, qrUserName, qrSaving, qrMsg, qrErr, qrQuery, showList, activeIdx`). +- También llama directamente `useGraphServices()`, `useSettingsHours()`, `useReservar()` y `useReporteria()` — es decir, no obtiene *todos* sus datos a través de `useAdminCells`. +- Combobox con navegación por teclado (`ArrowDown/Up/Enter/Escape`) y cierre diferido en `onBlur` (`setTimeout(..., 120)`) — un combobox accesible construido a mano, no un componente compartido. + +**Hallazgos de calidad**: +- `submitQuickReserve()` usa `console.log(...)` de depuración y un `alert()` bloqueante como única confirmación — no usa el `ToastProvider` ya disponible en la app. +- Cast `as any` sobre el payload de reserva (línea 152). +- Recalcula `turnNow` de forma independiente en vez de reusar `currentTurn` que ya calcula `useAdminCells`. +- `toggleEstado` (disponible en `useCeldas`) no tiene ninguna afordancia de UI en este archivo — no hay forma de activar/desactivar una celda desde la grilla. +- Mezcla tres responsabilidades (reserva rápida, filtros/paginación, render de badges) en un único archivo de 634 líneas. + +## `slotDetailsModal.tsx` (955 líneas) — modal de detalle de celda +El componente más grande del proyecto. Se abre desde `admin-cells.tsx` al pulsar "Detalles" sobre una celda. `Props = {open, slot: SlotUI|null, onClose, onChanged?, workers?, workersLoading?}`. + +Deriva el **modo** de operación de `Itinerancia`: `mode = 'reserva'` si contiene "itinerante", si no `'fijar'`. +- **Modo fijar**: busca colaborador sin celda (`useAsignarCeldas`), permite asignar/desasignar. +- **Modo reserva**: formulario de reserva puntual, con prellenado desde el directorio pasando por `nameProve()` (protección de guard `lastPickRef` con `Symbol()` para descartar respuestas obsoletas si el usuario elige rápido dos veces), y verificación de disponibilidad por turno (`countReservations`) antes de crear — esta verificación es la implementación concreta del commit *"Se estaba dejando reservar en celdas ocupadas desde el panel de admin"*. +- **Eliminar celda**: borra en cascada todas las reservas no canceladas de la celda (`Promise.allSettled`) y luego la celda misma. +- **Editar celda**: sub-modal anidado para cambiar `Itinerancia`/`Title`. + +**Hallazgos de calidad**: +- Instancia **sus propios** `ReservationsService`/`ColaboradoresFijosService`/`ParkingSlotsService` con constantes de sitio/lista hardcodeadas, en paralelo a los ya disponibles vía `useGraphServices()` (que este mismo archivo también importa para otro uso) — ver [ARQUITECTURA.md §5.2, punto 12](../ARQUITECTURA.md#52-alta--duplicación-estructural-y-decisiones-de-arquitectura). +- Estilos 100% en un objeto inline `S` (líneas 26-185), no CSS Modules — el único archivo grande del proyecto con este enfoque. +- El select de edición de celda contiene una opción con typo, `"Directico"` en vez de `"Directivo"` (usado en el resto de la app) — si se selecciona, escribe un valor de `Itinerancia` que ningún filtro reconoce. +- Ramas de error de `onCreateReservation` construyen `{ok:false, message}` que **el llamador nunca consume** — el único feedback real ante "sin disponibilidad" es un `alert()`. +- Varios `useCallback` con arrays de dependencias ajustados a mano e imprecisos (comentario propio: "quita lo que no usas", pero deja variables no usadas y omite otras que sí se usan). +- `finally { setRvSaving(false) }` en `onDeleteCell`/`onEditCell` sin que ninguna de las dos funciones jamás ponga `rvSaving` en `true` — no hay indicador de carga real para borrar/editar. + +## `useAdminCells.ts` (212 líneas) — hook de composición +Combina `useCeldas`, `useSettingsHours`, `useTodayOccupancy`, `useWorkers`; construye sus propias instancias de servicio (con las mismas constantes hardcodeadas que `slotDetailsModal.tsx`); fusiona filas con ocupación (`rowsWithOcc`); calcula capacidad "ahora mismo" (`capacidadAhora`, filtrando solo celdas `Activa`); gestiona un temporizador de auto-refresco de 5 minutos, pausado cuando la pestaña está oculta (`visibilitychange`). + +**Hallazgo crítico**: invoca hooks de React de forma **condicional**: +```ts +const c = slotsSvc ? useCeldas(slotsSvc) : { /* objeto de reemplazo */ }; +const occ = reservationsSvc ? useTodayOccupancy(reservationsSvc) : { /* ... */ }; +``` +Esto viola las Reglas de los Hooks (el número/orden de hooks invocados puede cambiar entre renders si `ready`/`slotsSvc` cambian) — ver [ARQUITECTURA.md §5.1, punto 7](../ARQUITECTURA.md#51-crítica--riesgos-de-seguridad-y-bugs-funcionales-activos). Funciona hoy porque `ready` se estabiliza pronto tras el login y no vuelve a cambiar en la vida de la sesión. + +Otros hallazgos: los objetos de reemplazo (líneas 50-82) duplican a mano toda la forma de retorno de `useCeldas`, sin verificación de tipos contra el hook real; `toggleEstado` no se reexpone desde este hook pese a estar disponible en `useCeldas`; `error: c.error || s.error || null` descarta silenciosamente `occ.error`. + +## Patrones de programación +- *Derived state* vía `useMemo` (fechas mínimas/máximas, opciones de trabajador, turno actual). +- Auto-refresco con `setInterval` + `visibilitychange` para pausar el polling en segundo plano. +- Cascada de borrado implementada del lado cliente (reservas → celda) con `Promise.allSettled`. + +## Deuda técnica destacada de este módulo +Ver el detalle priorizado (incluye este módulo como el de mayor riesgo de regresión) en [ARQUITECTURA.md §5](../ARQUITECTURA.md#5-deuda-técnica), puntos 7 y 12. diff --git a/docs/modulos/components-agregar-colaborador.md b/docs/modulos/components-agregar-colaborador.md new file mode 100644 index 00000000..7e58a755 --- /dev/null +++ b/docs/modulos/components-agregar-colaborador.md @@ -0,0 +1,24 @@ +# Módulo: `Components/AgregarColaborador` — Alta de colaborador fijo + +> Ver también: [Arquitectura general](../ARQUITECTURA.md) · [Índice de módulos](./README.md) · [Colaboradores](./components-colaboradores-permanentes.md) · [Servicios (`Name.Service`)](./services.md) + +Modal invocado desde [`Colaboradores.tsx`](./components-colaboradores-permanentes.md) para dar de alta un colaborador con celda fija. + +## `ModalAgregarColaborador.tsx` (296 líneas) +`Props = {isOpen, onClose, onSave?, slots?, slotsLoading?, workers?, workersLoading?}` — **`slots`/`slotsLoading` se declaran pero nunca se destructuran/usan en el cuerpo del componente** (prop muerta confirmada). + +- Combobox (filtro de texto libre `colabTerm` + `` en el formulario de creación y ningún control en la vista de lista/detalle. Este componente sería el candidato natural para cubrir ese hueco. diff --git a/docs/modulos/graph.md b/docs/modulos/graph.md new file mode 100644 index 00000000..37b48e92 --- /dev/null +++ b/docs/modulos/graph.md @@ -0,0 +1,42 @@ +# Módulo: Capa Graph (`src/graph`) + +> Ver también: [Arquitectura general](../ARQUITECTURA.md) · [Índice de módulos](./README.md) · [Servicios](./services.md) + +## `src/graph/GraphRest.ts` +Wrapper `fetch` genérico para Microsoft Graph API v1.0. Es la clase pensada como único cliente HTTP de la aplicación. + +```ts +class GraphRest { + constructor(getToken: () => Promise, baseUrl?: string) + get(path, init?) + post(path, body, init?) + patch(path, body, init?) + delete(path, init?) +} +``` + +El método privado `call()`: +- agrega `Authorization: Bearer ` (obtenido llamando a `getToken()` en cada request) y `Content-Type: application/json` si hay body, +- agrega el header `Prefer: HonorNonIndexedQueriesWarningMayFailRandomly` (permite que ciertas consultas OData no indexadas no fallen), +- maneja `204 No Content` devolviendo `undefined`, +- parsea la respuesta según `content-type` (JSON o texto), tolerando cuerpo vacío, +- ante error HTTP, intenta extraer `error.message` del cuerpo JSON de Graph y lanza un `Error` con mensaje detallado: `` `${method} ${path} → ${status} ${statusText}: ${detail}` ``. + +## `src/graph/GraphServicesContext.tsx` +El punto central de *dependency injection* de la aplicación. + +- `GraphSiteConfig`: `{hostname, sitePath, lists: {parkingSlots, colaboradoresFijos, reservations, usuariosParking, settings, picoYPlaca, registroVeh}}`, con valores por defecto en `DEFAULT_CONFIG` (línea 58): sitio `estudiodemoda.sharepoint.com/sites/TransformacionDigital/IN/SA`. +- `GraphServicesProvider`: crea un único `GraphRest` (memoizado sobre `getToken` del `AuthProvider`) y luego una instancia de cada `*Service` (memoizadas sobre `graph` + config), incluyendo `SharedServices`, que depende de `UsuariosParkingService` — la única composición explícita servicio→servicio realizada vía constructor. +- `useGraphServices(): GraphServices` — hook de consumo; lanza error si se usa fuera del provider. + +Servicios expuestos: `graph, parkingSlots, colaboradoresFijos, reservations, usuariosParking, settings, picoYPlaca, shared, registroVeh`. + +## Patrones de programación +- *Dependency Injection* vía React Context: un único punto (`GraphServicesProvider`) construye el grafo de dependencias de toda la capa de datos. +- Memoización agresiva (`useMemo`) para evitar recrear instancias de servicio en cada render. +- *Config override* opcional (`config?: Partial`) permitiendo, en teoría, apuntar a otro sitio/listas sin tocar código — aunque en la práctica no se usa en ningún punto de montaje. + +## Notas de calidad (deuda técnica) +- Los valores por defecto de `hostname`/`sitePath`/nombres de lista definidos aquí en `DEFAULT_CONFIG` **se repiten** como valores por defecto en el constructor de cada `*Service` individual (ver [Servicios](./services.md)) — dos fuentes de la misma configuración que podrían divergir si se edita una sin la otra. +- No hay actualmente ningún otro archivo que apunte a un sitio/lista distinto — el mecanismo de `config` override existe pero no se usa, es una capacidad no ejercitada. +- Además de `GraphRest`, existen 3-4 clientes HTTP independientes hacia Graph en otras partes del código (`Services/GraphUsers.service.ts`, `Services/GruposCorreo.service.ts`, `Hooks/GraphUsers.ts`, `Hooks/useWorkers.ts`, `Hooks/usePicoPlaca.ts`) que no pasan por `GraphRest` ni por este contexto — ver [Servicios](./services.md) y [Hooks](./hooks.md). diff --git a/docs/modulos/hooks.md b/docs/modulos/hooks.md new file mode 100644 index 00000000..a06a0218 --- /dev/null +++ b/docs/modulos/hooks.md @@ -0,0 +1,62 @@ +# Módulo: Hooks (`src/Hooks`) + +> Ver también: [Arquitectura general](../ARQUITECTURA.md) · [Índice de módulos](./README.md) · [Servicios](./services.md) · [Models](./models.md) + +Los hooks encapsulan estado de React (loading/error/rows/paginación) alrededor de un Service. La mayoría recibe el Service **por parámetro** (inyección explícita); algunos (`useSettingHour`, `useWorkers`, `GraphUsers.ts`, `usePicoPlaca.ts`) acceden a Graph directamente vía `useAuth()`/`useGraphServices()`/`fetch` propio, sin pasar por un Service — dos convenciones de inyección de dependencias coexistiendo en la misma capa. + +## `useCeldas.ts` — el hook más completo +CRUD + búsqueda + filtros (`tipo`, `itinerancia`, `estado`) + paginación para la lista de celdas (`ParkingSlotsService`). *Debounce* de búsqueda (300 ms) vía `useRef`+`setTimeout`; usa un **`reqIdRef`** ("la última petición gana") para descartar respuestas obsoletas — el patrón de concurrencia más robusto de toda la capa de Hooks. `create()` implementa la regla de negocio: crear una celda `Moto` genera **5 sub-celdas** sufijadas `A`-`E` (`Promise.allSettled`); crear una `Carro` genera solo 1. `toggleEstado` reporta error vía `alert()` en vez de usar el `error` de estado del propio hook. Filtro `estado` (Activa/Inactiva/Todas) es la adición más reciente de la app (commit "Bloqueo de reservas"). + +## `useColaboradores.ts` (exporta `useCollaborators`) +Trae **todo** el listado de la lista `Colaboradores fijos` (`top: 2000`) y filtra/pagina 100% en cliente sobre un `masterRef` (caché en memoria fuera de React state), con normalización de acentos (`normalize('NFD')`) para la búsqueda. Inserta de forma optimista el nuevo colaborador en `masterRef` tras `addCollaborator`. El nombre exportado (`useCollaborators`, inglés) no coincide con el nombre de archivo (español). + +## `useRegistroVehicular.ts` +Estructuralmente casi idéntico a `useColaboradores.ts` (mismo patrón `masterRef` + debounce 250ms + filtro local), aplicado a la lista `RegistroVehicular` — duplicación de código casi total entre ambos hooks, con nombres de función (`mapToCollaborator`) copiados sin renombrar. + +## `useMisReservas.ts` +Soporta dos modos: `upcoming-active` (reservas futuras y activas del usuario) e `history` (rango de fechas libre, cualquier estado). No-admins quedan fijados a su propio email en el filtro OData (`fields/Title eq ''`). `cancelReservation(id)` actualiza `Status` a `'Cancelada'` y refresca. La paginación (`pageIndex`/`pageSize`/`hasNext`) se calcula pero **no se aplica** — el componente consumidor siempre recibe el arreglo completo de filas. Contiene un `console.log` de depuración con las filas obtenidas. + +## `useReservar.ts` — núcleo del flujo de reserva +`useReservar(reservationsSvc, slotsSvc, settingsSvc, userMail, userName)` → `{minDate, maxDate, loading, error, reservar, countReservations}`. +- Ventana de reserva: `minDate = hoy`, `maxDate = hoy + Settings.VisibleDays` (default 3 si falta/es inválido). +- Regla "una reserva por usuario por día": bloquea un "Día completo" si ya existe cualquier reserva activa ese día; bloquea un turno si ya hay una reserva en ese turno o un "Día completo" ya existente. +- Orden de asignación de celdas: un conjunto hardcodeado `LAST_GROUP_IDS = new Set([5])` se deja para el final de la lista de candidatas — regla de negocio sin documentar en ningún lado más que en el propio código. +- Verifica disponibilidad contando reservas activas por celda/turno candidato (recorrido secuencial, no paralelo — el tiempo de búsqueda crece linealmente con el número de celdas activas). +- Genera `Codigo` contando **todas** las reservas existentes (`getAll({top: 20000})`) y sumando 1 — no atómico, condición de carrera bajo reservas concurrentes (ver [ARQUITECTURA.md §5.1, punto 3](../ARQUITECTURA.md#51-crítica--riesgos-de-seguridad-y-bugs-funcionales-activos)). +- En cada montaje dispara además un `getAll({top:2000})` completo solo para hacer `console.log` de depuración del primer registro — costo de red real pagado únicamente para debugging. +- Redeclara localmente sus propios `VehicleType`/`TurnDb` en vez de reusar `Models/shared.ts::VehicleType`/`TurnType`. + +## `useReportes.ts` (exporta `useReporteria` — reporte de aforo/ocupación) +Combina `ReservationsService` + `ParkingSlotsService`. `loadCapacidad()`: capacidad total = (# celdas activas Carro) + (# celdas activas Moto × `MOTO_CAPACITY`, constante hardcodeada en 1). El cálculo de "% de aforo" suma 1 unidad por reserva sin distinguir tipo de vehículo — las tres ramas de un `if/else` de tipo de vehículo son en la práctica equivalentes (rama muerta: `unidades += 1` en los tres casos). + +## `reporteRegistro.ts` (exporta `useReporteria` — reporte de registro vehicular; **nombre duplicado** con el hook anterior**) +Filtra la lista `RegistroVehicular` por rango de fecha (`Created`), tipo de vehículo y nombre (`startswith`, no `contains`). Contiene dos bloques `useEffect` con cuerpo idéntico (uno "carga inicial", otro "recarga al cambiar filtro") — ambos disparan la misma recarga de forma redundante. Filtra por `fields/TipeVeh` (posible typo respecto a `TipoVeh`, usado en otros lugares) — riesgo de estar filtrando un campo que no existe. + +## `useTodayOccupancy.ts` +Calcula ocupación de celdas **para hoy** por turno (AM/PM) a partir de reservas activas, para el dashboard de capacidad. Cualquier valor de turno no reconocido como mañana/tarde (p. ej. "Día completo") marca **ambos** turnos como ocupados. Formatea fechas manualmente en vez de reusar `utils/date.ts`. + +## `useSettingHour.ts` (exporta `useSettingsHours`) +Trae y normaliza los 4 campos de horario (`InicioManana/FinalManana/InicioTarde/FinalTarde`) del ítem de Settings (id fijo `"1"`), tolerando formatos `"07:00"`, `"7"` o `7` (`toHourNumber`). Es el único hook CRUD que obtiene su Service directo de `useGraphServices()` en vez de recibirlo por parámetro — convención de DI distinta al resto de la capa. Duplica lógica de `clampHour` ya existente en `Models/time.ts`. + +## `useAsignarCeldas.ts` +Pese al prefijo `use*` y vivir en `Hooks/`, **no es un hook de React** (no usa `useState`/`useEffect`) sino un módulo de funciones async puras para asignar/desasignar una celda fija a un colaborador (`fetchAssignee`, `assignSlotToCollaborator`, `unassignSlotFromCollaborator`, `searchUnassignedCollaborators` con doble filtro server+cliente "por si el backend no filtró bien"). + +## `useWorkers.ts` +Directorio completo de usuarios de Microsoft 365 para *pickers*/autocompletado. Implementa una **caché global a nivel de módulo** (`const cache = {}` fuera del ciclo de vida de React, con deduplicación de promesas en vuelo) — nunca se invalida al cerrar sesión/cambiar de usuario, riesgo de fuga de datos de directorio entre sesiones si no hay recarga completa de página. + +## `usePicoPlaca.ts` — nombre engañoso +En realidad exporta `useRecipients`, un hook para armar listas de destinatarios de correo desde el directorio de Graph (filtro de dominio, deduplicación, límite de previsualización). **No tiene relación con la funcionalidad de Pico y Placa**; la pantalla de Pico y Placa (`Components/PicoPlaca/PicoPlaca.tsx`) no lo importa y reimplementa su propia validación en el componente. + +## `GraphUsers.ts` — nombre engañoso +En realidad exporta `useGroupMembers(groupId)`: lista/pagina/busca miembros de un grupo de Azure AD y expone `addCollaboratorByUserId`/`deleteByUserId`/`deleteByEmail`. Implementa su propio cliente `fetch` (no pasa por ningún Service ni por `GraphRest`). Colisiona en nombre con `Services/GraphUsers.service.ts` y con `Models/GraphUsers.ts` (conceptos distintos en los tres casos). + +## `utils.ts` +Funciones puras compartidas: `dayLabel(title)` (día de la semana en español), `isValidPattern(v)` (valida listas de dígitos separadas por coma — la validación que, en teoría, debería usar la pantalla de Pico y Placa), `normalizeResult(res)` (normaliza distintas formas de resultado `{ok}`/`{success}` que devuelven los distintos Services, evidencia de que el contrato de retorno entre servicios no es uniforme). + +## Patrones de programación +- Estado *loading/error/rows* + acciones expuestas como `useCallback`, repetido en casi todos los hooks CRUD. +- Tres estrategias de seguridad de concurrencia distintas coexisten: bandera booleana `cancel` en cleanup del efecto (la mayoría), `reqIdRef` de "última petición gana" (`useCeldas.ts`), o ninguna (`useTodayOccupancy`, `useWorkers`, `usePicoPlaca.ts`). Ninguna usa `AbortController` para cancelar el `fetch` subyacente. +- Caché en memoria fuera de React (`masterRef`, caché de módulo en `useWorkers.ts`) para evitar refetch en filtros/búsquedas locales. + +## Notas de calidad (deuda técnica) — resumen +Ver detalle completo en [ARQUITECTURA.md §5](../ARQUITECTURA.md#5-deuda-técnica). En síntesis: el esqueleto de fetch/filtro/paginación/debounce está duplicado casi verbatim en `useCeldas.ts`, `useColaboradores.ts` y `useRegistroVehicular.ts`; hay dos hooks distintos llamados `useReporteria`; dos archivos (`usePicoPlaca.ts`, `GraphUsers.ts`) tienen nombre que no corresponde a lo que exportan; y la lógica de reserva (`useReservar.ts`) tiene riesgos de condición de carrera con impacto de negocio real. diff --git a/docs/modulos/models.md b/docs/modulos/models.md new file mode 100644 index 00000000..732e1a96 --- /dev/null +++ b/docs/modulos/models.md @@ -0,0 +1,34 @@ +# Módulo: Models (`src/Models`) + +> Ver también: [Arquitectura general](../ARQUITECTURA.md) · [Índice de módulos](./README.md) · [Hooks](./hooks.md) · [Servicios](./services.md) + +Debería ser una capa de solo tipos/contratos; en la práctica también contiene lógica de negocio y, en un caso, un hook completo. Se documentan todos los archivos; ver [ARQUITECTURA.md §5.3](../ARQUITECTURA.md#53-media--inconsistencias-y-código-muerto) para el resumen de inconsistencias de nombres de campo entre modelos. + +| Archivo | Contenido | Nota | +|---|---|---| +| `Commons.ts` | `GetAllOpts {filter?, orderby?, top?}` | tipo compartido correctamente reusado por la mayoría de Services | +| `shared.ts` | `VehicleType`, `TurnType`, `Worker` | pensado como fuente única de tipos de dominio; no todos los hooks lo usan (p. ej. `Hooks/useReservar.ts` redeclara sus propios `VehicleType`/`TurnDb` locales) | +| `Celdas.ts` | `SlotUI`, `Assignee`, `TurnFlags`, `CreateForm`, `Mode`, `mapSlotToUI()` | modelo de vista + mapeador para la pantalla de celdas; `Raw: any` en `SlotUI` es un escape de tipado | +| `Colaboradoresfijos.ts` | `Colaboradoresfijos {ID, Title?, Correo?, Tipodevehiculo, Placa?, CodigoCelda?, SpotAsignado?}` | campo `Tipodevehiculo` (todo junto, minúscula) rompe la convención PascalCase del resto del archivo | +| `Parkingslot.ts` | `ParkingSlot {ID: string, Title?, TipoCelda?, Itinerancia?, Activa?}` | `ID: string`, inconsistente con `Colaboradoresfijos.ID: number` — el mismo concepto ("id de ítem SharePoint") tipado distinto según el modelo | +| `PicoPlaca.ts` | `PicoYPlaca {ID: string, Title?, Moto?, Carro?}` | duplicado por un tipo local equivalente (`PicoPlacaRow`) definido dentro de `Components/PicoPlaca/PicoPlaca.tsx` en vez de importar este | +| `RegistroVehicular.ts` | `RegistroVehicularSP` (modelo crudo) + `RegistroVehicularMail` (payload de correo) | dos convenciones de nombre de campo distintas en el mismo archivo (PascalCase SP vs. camelCase de payload) | +| `Reservation.ts` | `Reservations` (crudo, con columnas *lookup*), `ReservationUI` + `mapReservationToUI()`, `ReserveArgs`, `ReserveResult` (unión discriminada `{ok:true|false}`) | segundo mapeador "crudo→UI" independiente del que define `Hooks/useMisReservas.ts` (`mapModelToUI`) — dos implementaciones de la misma conversión | +| `Reportes.ts` | `Filtros`, `Row`, `ReservaUI` | `Row` no parece tener consumidores; `ReservaUI` es un **tercer** modelo de vista de "reserva para tabla", con nombres de campo distintos (`Fecha`/`Turno`/`Celda`) a los de `ReservationUI` (`Date`/`Turn`/`Spot`) | +| `Settings.ts` | `FormState` (incluye el artefacto de SharePoint `Ma_x00f1_ana`), `Props` | **importa un tipo desde `Services/Setting.service.ts`**, invirtiendo la dirección de dependencia Models→Services que se espera en la arquitectura | +| `UsuariosParking.ts` | ~30 campos, en su mayoría metadatos de sistema de SharePoint/Power Platform (`OData__ColorTag`, `{ModerationStatus}`, `{Attachments}`, etc.) | solo 4 campos (`ID, Title, Rol, Permitidos`) son poblados realmente por `UsuariosParkingService.toModel()` — el resto es ruido, aparentemente pegado desde un esquema autogenerado | +| `GraphUsers.ts` | `GraphUser`, `GraphListResponse`, `OData`, `GraphUserLite`, `newAccess` | nombre colisiona con `Hooks/GraphUsers.ts` y `Services/GraphUsers.service.ts` (conceptos distintos en cada capa); `newAccess` rompe la convención PascalCase de nombres de tipo | +| `users.ts` | `UserMe` (perfil `/me`) | distinto de `GraphUsers.ts` (listado de directorio); el trío `users.ts`/`GraphUsers.ts`/`UsuariosParking.ts` para tres conceptos distintos genera fricción de navegación por similitud de nombres | +| `colaboradores.ts` | `Collaborator`, `NewCollaborator` (vista de UI) | mezcla camelCase (`nombre`, `correo`) y PascalCase (`CodigoCelda`, `IdSpot`) dentro del mismo tipo | +| `misReservas.ts` | `FilterMode` | **código muerto**: nadie lo importa; `Hooks/useMisReservas.ts` redeclara el mismo tipo literal localmente en vez de importarlo | +| `ModalModel.ts` | `reserveModal` (props de modal genérico de confirmación) | nombre de tipo en minúscula inicial (rompe convención) y engañoso: no es específico de reservas, lo usa un modal genérico | +| `Modals.ts` | `Props` (props del modal de detalle de colaborador) | exporta un identificador genérico `Props`; nombre de archivo casi idéntico a `ModalModel.ts` para un contrato no relacionado | +| `time.ts` | `Hours`, `fmtHour`, `deriveHoursLabels`, `getCurrentTurnFromHours` | contiene lógica de negocio, no solo tipos (atípico para "Models"); duplica el `clampHour` que también define, por separado, `Hooks/useSettingHour.ts` | +| `toast.ts` | `Toast`, `ToastKind` | limpio, sin hallazgos | +| `useCeldas.ts` | **un hook de React completo**, no un tipo (`useState`/`useEffect`/`useCallback`, CRUD, paginación) | copia antigua/desactualizada de `Hooks/useCeldas.ts` (sin filtro `estado`, sin debounce, sin `reqIdRef`); **código muerto** confirmado — nadie lo importa — pero comparte el mismo nombre exportado (`useCeldas`) que el hook real, riesgo de importarlo por error | + +## Patrones de programación +- Separación nominal entre "modelo crudo" (mirror de columnas SharePoint) y "modelo de vista" (`*UI`) con una función `map*ToUI()` — patrón correcto en principio, aplicado de forma inconsistente (a veces hay 2-3 mapeadores para el mismo concepto, ver tabla arriba). + +## Notas de calidad (deuda técnica) — resumen +El problema principal de esta capa es *scope creep*: además de tipos, alberga lógica de negocio (`time.ts`), un hook completo huérfano (`useCeldas.ts`), un tipo muerto de una sola línea (`misReservas.ts`) y un esquema sobre-extendido copiado de una herramienta externa (`UsuariosParking.ts`). Ver el detalle priorizado en [ARQUITECTURA.md §5.3, puntos 16, 22-24](../ARQUITECTURA.md#53-media--inconsistencias-y-código-muerto). diff --git a/docs/modulos/ports.md b/docs/modulos/ports.md new file mode 100644 index 00000000..662d5a00 --- /dev/null +++ b/docs/modulos/ports.md @@ -0,0 +1,33 @@ +# Módulo: Ports (`src/Ports`) + +> Ver también: [Arquitectura general](../ARQUITECTURA.md) · [Índice de módulos](./README.md) · [Servicios](./services.md) + +Capa de adaptación **parcial**: solo existe para 2 de los ~11 servicios de dominio (`PicoYPlacaService`, `SettingsService`). El resto de servicios se consume directo desde componentes/hooks vía `useGraphServices()`, sin pasar por ningún Port. Ver [ARQUITECTURA.md §5.2, punto 11](../ARQUITECTURA.md#52-alta--duplicación-estructural-y-decisiones-de-arquitectura) para el análisis de por qué esto no constituye una capa arquitectónica consistente. + +## `PicoPlaca.port.ts` +`makePicoPlacaPort(svc: PicoYPlacaService): PicoPlacaPort` adapta el servicio a un `PicoPlacaRow`/`PicoPlacaPort` orientado a UI (`getAll`, `update`). + +- `toRow()` (líneas 19-24) vuelve a resolver mayúsculas/minúsculas de campo (`i.Moto ?? i.moto`, `i.Carro ?? i.carro`) que el propio `toModel()` del servicio **ya normaliza** a PascalCase — código defensivo redundante que nunca se activa en la práctica (las variantes en minúscula nunca llegan del servicio). +- Re-ordena numéricamente en cliente (`rows.sort((a,b) => Number(a.Title) - Number(b.Title))`) tras pedir `orderby: 'fields/Title asc'` al servicio, porque OData ordena `Title` como texto (`"1","10","2",...`) — corrección legítima, pero delatando que el `orderby` del servicio subyacente no sirve para valores numéricos. +- `update()` es un paso directo, sin observaciones. + +## `settingsPort.ts` +`makeSettingsPortSingle(svc?: SettingsService): SettingsPort` adapta `SettingsService` a un formulario de UI (`SettingsForm`/`SettingsRecord`). + +**Bug de integridad de datos confirmado**: el payload de creación por defecto usa un campo `TyC` (no `TerminosyCondiciones`, el nombre real del modelo de `SettingsService`) y horas numéricas (`7`, `12`) en vez de strings `"HH:mm"` — todo forzado con `as any` para evadir el chequeo de tipos: +```ts +const created = await svc.create({ + VisibleDays: 7, TyC: '', InicioManana: 7, FinalManana: 12, + InicioTarde: 12, FinalTarde: 18, PicoPlaca: false, +} as any); +``` +Como `SettingsService.toModel()` nunca produce un campo `TyC`, la lectura posterior `rec.TyC ?? rec.TerminosyCondiciones` (líneas 47, 60) siempre cae al segundo operando — el valor de `TyC` escrito aquí **nunca se recupera**, y los valores numéricos de hora quedan en un formato distinto al que espera el resto de la app (`"HH:mm"`). + +Riesgo adicional (no confirmado contra el esquema real de SharePoint): `svc.getAll({top: 1, orderby: 'fields/ID asc'})` ordena por `fields/ID`, mientras que el id de un ítem de SharePoint normalmente se expone en el nivel superior (`id`), no anidado bajo `fields` — puede que este `$orderby` no corresponda a ningún campo indexado real. + +## Patrones de programación +- Función *factory* (`make*Port(svc)`) que cierra sobre el servicio real y devuelve un objeto con métodos de UI — un patrón de adaptador ligero, sin clases. + +## Notas de calidad (deuda técnica) +- El Port no elimina duplicación de lógica OData/CRUD (delega correctamente en el Service), pero sí duplica y a veces **contradice** las suposiciones de forma de campo del Service que envuelve — ver el caso `TyC` arriba. +- Al cubrir solo 2 de ~11 servicios, "Port" no es una capa consistente de la arquitectura sino una envoltura aplicada ad hoc a dos pantallas específicas (Settings admin, Pico y Placa admin). diff --git a/docs/modulos/services.md b/docs/modulos/services.md new file mode 100644 index 00000000..8641e239 --- /dev/null +++ b/docs/modulos/services.md @@ -0,0 +1,62 @@ +# Módulo: Servicios (`src/Services`) + +> Ver también: [Arquitectura general](../ARQUITECTURA.md) · [Índice de módulos](./README.md) · [Capa Graph](./graph.md) · [Ports](./ports.md) + +Cada servicio de lista sigue el mismo patrón base ("repository" por lista de SharePoint): constructor `(graph: GraphRest, hostname, sitePath, listName)` con defaults hardcodeados; método privado `ensureIds()` que resuelve `siteId`/`listId` una vez (vía `/sites/{hostname}:{sitePath}` y filtro por `displayName`) y los cachea en `localStorage` (clave `sp:{hostname}{sitePath}:{listName}`); CRUD estándar (`get/getAll/create/update/delete`) construido sobre `GraphRest`; `getAll(opts: GetAllOpts)` arma `$filter/$orderby/$top/$expand=fields` a partir de [`Models/Commons.ts::GetAllOpts`](./models.md). + +## `ParkingSlotsService` (`ParkingSlot.service.ts`) +Repositorio de la lista `ParkingSlots` (celdas de parqueo). `getAll()` es el *query builder* más elaborado del proyecto: reescribe tokens de filtro/orderby (`ID`→`id`, `Title`→`fields/Title`), re-escapa literales, sustituye `+` por `%20` en la query final ("porque algunos proxies se quejan", según comentario del código), y si el primer intento lanza `itemNotFound` **reintenta sin `$filter`**, degradando silenciosamente a una consulta sin filtrar. También expone `findByCodigo(codigo, top=1)` y `getDisponibles(top=100)` (`filter: 'fields/Disponible eq true'`). + +- *Nota de consistencia*: resuelve el sitio con dos-puntos finales (`/sites/{hostname}:{sitePath}:`), a diferencia del resto de servicios de esta lista, que no lo usan — indicio de que el bloque `ensureIds` ya divergió entre copias. + +## `ColaboradoresFijosService` (`Colaboradoresfijos.service.ts`) +Repositorio de la lista `Colaboradores fijos`. Además del CRUD estándar, expone `getAllPaged(opts)`/`getNextPage(nextLink)` con paginación manual vía `@odata.nextLink` (recorta el host hardcodeado del link antes de reenviarlo a `GraphRest`, que lo vuelve a anteponer). + +## `ReservationsService` (`Reservations.service.ts`) +Repositorio de la entidad central de negocio, la lista `Reservations`. +- `toModel` mapea columnas *lookup* (`SpotIdLookupId`, `AuthorLookupId`/`EditorLookupId`) con guards de tipo (`typeof f.X === 'number' ? f.X : null`). +- Contiene un *fallback* sin efecto: `VehicleType: f.VehicleType ?? f.VehicleType` (comparación de una expresión consigo misma) — casi seguro un resto de una segunda variante de nombre de campo que nunca se completó. +- `create()` deja `console.table(record)`/`console.log(...)` de depuración en el flujo de producción. +- `reservationCode(number)` calcula `format5(number + 1)`; el llamador (`slotDetailsModal.tsx`) ya incrementa `total` en 1 antes de invocar `create()` — el resultado es un desfase de +2 respecto al conteo real, y además una condición de carrera si dos reservas se crean casi simultáneamente (ver [ARQUITECTURA.md §5.1, punto 3](../ARQUITECTURA.md#51-crítica--riesgos-de-seguridad-y-bugs-funcionales-activos)). +- Consultas auxiliares: `findBySpotId`, `findByDateRange`, `findByUser`. + +## `SettingsService` (`Setting.service.ts`) +Repositorio de la lista `Settings` (registro único de configuración global). Redefine localmente su propia interfaz `Settings` y su propio `GetAllOpts` en vez de importar el tipo compartido de `Models/Commons.ts` — inconsistencia respecto al resto de servicios. `toModel` aplica horarios por defecto hardcodeados (`'07:00'`/`'12:00'`/`'12:00'`/`'18:00'`) si el campo viene nulo. `getVisibleDaysFromId1()` asume que el ítem de configuración siempre tiene `id='1'` — frágil si la fila se recrea alguna vez en SharePoint (recibiría un nuevo id). + +## `PicoYPlacaService` (`PicoPlaca.ts`, sin sufijo `.service`) +Repositorio de la lista `Pico y Placa`. Es el único archivo de Services que aloja lógica de negocio "de dominio" real: +- `findByDia(dia, tipoVehiculo?, onlyActive=true)`: arma el `$filter` combinando día, tipo de vehículo y estado activo. +- `isRestringido(placa, fecha, tipoVehiculo?)`: calcula el día de la semana en español con un array hardcodeado, extrae el último dígito de la placa, evalúa listas/rangos de dígitos y ventanas horarias con *parsing* manual (`HH:mm` → minutos). Sin ventana horaria configurada, la regla se interpreta como "restringido todo el día" (documentado solo en un comentario, no en el modelo ni en tests). +- Comentarios propios reconocen incertidumbre sobre el esquema real de la lista ("ajusta a tu formato real si es distinto"). + +## `RegistroVehicularService` (`RegistroVehicular.service.ts`) +Repositorio de la lista `RegistroVehicular`. `toModel` mapea correctamente a `RegistroVehicularSP`, pero **`create`/`update` están tipados contra `Omit`** — residuo evidente de haber copiado el archivo de `Colaboradoresfijos.service.ts` sin actualizar los tipos genéricos. TypeScript no puede detectar campos mal escritos en las llamadas a estos dos métodos. Ver [ARQUITECTURA.md §5.1, punto 4](../ARQUITECTURA.md#51-crítica--riesgos-de-seguridad-y-bugs-funcionales-activos). + +## `UsuariosParkingService` (`UsuariosParking.service.ts`) +Repositorio de la lista `usuariosparking` (rol/permiso por usuario). `toModel` solo puebla `ID, Title, Rol, Permitidos` de los ~30 campos que declara `Models/UsuariosParking.ts`. `findByCorreo(correo)` filtra por un campo `Correo` que `toModel` **nunca** lee/expone — inconsistencia interna entre el método de búsqueda y el modelo resultante (posible método muerto/roto). + +## `SharedServices` (`Shared.service.ts`) +No representa una lista propia: envuelve `UsuariosParkingService` (inyectado por constructor, única composición explícita servicio→servicio del proyecto) para exponer `getRole(userEmail)`. Busca por `fields/Title eq ''` (con variante `tolower` como *fallback*) — es decir, el email del usuario se guarda en el campo `Title` de `usuariosparking`, una convención implícita no documentada en el modelo. Ante cualquier error de red/consulta, **retorna `null` de forma silenciosa y sin log** — usado para autorización, este es un punto sensible (ver [ARQUITECTURA.md §3.2](../ARQUITECTURA.md#32-autorización-roles)). + +## `UserService` (`User.Service.ts`) +Envuelve los endpoints `/me` de Graph. `getMeBasic()` trae el perfil básico tipado (`Models/users.ts::UserMe`). `getMyPhotoDataUrl()` **rompe encapsulamiento**: accede a `(this.graph as any).getToken()` (miembro privado de `GraphRest`) para hacer un `fetch` binario directo a `/me/photo/$value`, ya que `GraphRest` solo sabe parsear JSON/texto. Errores completamente silenciados (`catch { return null }`). + +## `GraphUsers.service.ts` — funciones sueltas, no una clase +Pese al sufijo `.service.ts`, exporta funciones planas (no una clase): `removeMemberByUserId`, `getUserIdByEmail`, `removeMemberByEmail`, `removeMembersBulk` (recorre en **serie**, no en paralelo, un arreglo de ids/emails). Implementa su propio cliente `fetch` (bypasea `GraphRest`). No está registrado en `GraphServicesContext` — se importa directo desde `Components/PermisosApp/usuariosApp.tsx`. Su propio comentario de cabecera dice `// src/Services/GroupMembers.service.ts`, desalineado con el nombre real del archivo. + +## `GruposCorreo.service.ts` +Una única función, `getGroupUsers(groupID, getToken, transitive=true)`, para listar miembros (transitivos o directos) de un grupo de Azure AD, paginando por `@odata.nextLink` y filtrando a `@odata.type` de usuario. **Código muerto**: ningún archivo del proyecto la importa (`Hooks/GraphUsers.ts` reimplementa la misma lógica de forma independiente, ver [Hooks](./hooks.md)). + +## `Log.service.ts` +Duplicado byte-a-byte de `Colaboradoresfijos.service.ts` (mismo hash MD5, misma clase exportada `ColaboradoresFijosService`). No implementa ninguna funcionalidad de logging pese a su nombre. No se usa en ningún lado — candidato directo a eliminar. Ver [ARQUITECTURA.md §5.2, punto 9](../ARQUITECTURA.md#52-alta--duplicación-estructural-y-decisiones-de-arquitectura). + +## `Name.Service.ts` +`nameProve(name): Promise` — declarado `async` sin ningún `await` interno (síncrono en la práctica). Normaliza acentos/mayúsculas y rechaza nombres que coincidan (exacto o por substring) con una lista negra hardcodeada de palabras genéricas (`practicante, aprendiz, admin, facturas, cedi, ...`). Se usa para evitar asignar celdas/reservas a cuentas de rol/genéricas en vez de personas reales — aplicado de forma inconsistente entre los distintos modales que seleccionan colaboradores del directorio (ver componentes en el índice). + +## Patrones de programación +- *Repository pattern* por lista SharePoint, con resolución perezosa de ids (`ensureIds`) cacheada en `localStorage`. +- *Query builder* manual sobre OData (`$filter`/`$orderby`/`$top`/`$expand`) — sin una librería de queries tipada. +- Un único punto de composición servicio→servicio (`SharedServices` sobre `UsuariosParkingService`); el resto de servicios son independientes entre sí. + +## Notas de calidad (deuda técnica) — resumen +Ver detalle completo en [ARQUITECTURA.md §5](../ARQUITECTURA.md#5-deuda-técnica). En síntesis: el bloque `ensureIds`/caché/escape está duplicado literalmente en 7-8 archivos sin una clase base compartida; existen 3-4 clientes HTTP paralelos a `GraphRest`; el manejo de errores va de silencioso (`catch {return null}`) a coincidencia de texto sobre mensajes de error (`msg.includes("404")`); y el tipado `any` es extendido en casi todos los `toModel(item: any)` y payloads de escritura. diff --git a/docs/modulos/utils.md b/docs/modulos/utils.md new file mode 100644 index 00000000..61b4eec1 --- /dev/null +++ b/docs/modulos/utils.md @@ -0,0 +1,41 @@ +# Módulo: Utils (`src/utils`) + +> Ver también: [Arquitectura general](../ARQUITECTURA.md) · [Índice de módulos](./README.md) + +Helpers puros, sin estado ni dependencia de React (salvo `SendEmail.ts`, que recibe un cliente Graph-like por parámetro). + +## `date.ts` +- `ymdLocal(d: Date)`: formatea una fecha local como `YYYY-MM-DD`. +- `last30Days()`: retorna `{from, to}` cubriendo los últimos 30 días. +- `addDays(date, days)`. +- `toISODate(d: Date|null)` / `todayISO()`. +- `formatDateTime(iso, locale='es-CO')` / `formatDateLocal(iso, locale='es-CO')` / `formatDateUTC(iso, locale='es-CO')`: formatean usando `Intl.DateTimeFormat`, fijando la zona horaria a `America/Bogota` (los dos primeros) o `UTC` (el tercero, para evitar que una fecha "solo fecha" se corra de día por huso horario). +- `formatRelative(iso, locale='es')`: formato relativo tipo "hace 3 horas" vía `Intl.RelativeTimeFormat`. + +## `exportExcel.ts` +`exportRowsToExcel(rows: T[], filename='reporte.xlsx')`: usa la librería `xlsx` (SheetJS) para convertir un arreglo de objetos a una hoja (`json_to_sheet`), calcula un autoancho simple de columna (basado en la longitud máxima de contenido, acotado entre 10 y 60 caracteres) y descarga el archivo (`XLSX.writeFile`). Usado por ambas pantallas de Reportes. + +## `number.ts` +`format5(n: number|string)`: formatea un número como cadena de 5 dígitos con ceros a la izquierda, vía `Intl.NumberFormat` con `minimumIntegerDigits: 5`. Usado para generar el código legible de reserva. + +## `resolveUserUpnOrId.ts` +`resolveUserUpnOrId(graph, {email?, useCurrent?})`: dado un cliente Graph-like (`{get}`), resuelve `{upn, id}` de un usuario por su email (`$filter=mail eq '...'`) o del usuario actual (`/me`). + +## `SendEmail.ts` +Construye y envía el correo HTML de "solicitud de inscripción de vehículo": +- `buildHtml(...)`: arma el cuerpo HTML con escape básico de entidades (`esc()`). +- `buildPayload(data: RegistroVehicularMail)`: arma el payload de Graph `sendMail`. +- `sendRegistroVehicularEmail(graph, data)`: envía desde el usuario actual (`/me/sendMail`). +- `sendRegistroVehicularEmailFrom(graph, userKey, data)`: envía desde un buzón específico (`/users/{userKey}/sendMail`). +- Soporta tanto un cliente `GraphRest`-like (`.post()`) como un cliente tipo SDK (`.api().post()`) mediante un *type guard* (`isGraphRest`). + +## `status.ts` +`statusColor(status: string)`: mapea substrings de estado (`cancel`, `termin`, `act`) a colores hexadecimales para badges de UI; cualquier otro valor cae a un azul por defecto. + +## Patrones de programación +- Funciones puras, sin efectos secundarios (salvo el envío de correo y la descarga del Excel), fácilmente reutilizables y testeables en aislamiento — el módulo con menor deuda técnica de todo el proyecto. +- Uso consistente de las APIs `Intl.*` del navegador en vez de librerías de fecha externas (no hay `date-fns`/`dayjs` en las dependencias). + +## Notas de calidad +- `date.ts` fija la zona horaria a `America/Bogota` de forma hardcodeada — correcto para una app de un solo país/oficina, pero a tener en cuenta si la empresa opera en otra zona horaria en el futuro. +- Ningún otro módulo del proyecto reutiliza sistemáticamente estos helpers: varios hooks (`useTodayOccupancy.ts`, partes de `useMisReservas.ts`) formatean fechas manualmente en vez de importar `ymdLocal`/`todayISO` desde aquí — ver [Hooks](./hooks.md). diff --git a/src/App.tsx b/src/App.tsx index 783f0607..487bc809 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,8 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import './App.css'; +import Availability from './Components/Reservar/Reservar'; import MisReservas from './Components/Mis-Reservas/mis-reservas'; -import ReservationsDisabledNotice from './Components/Notices/ReservationsDisabledNotice'; import AdminCells from './Components/AdminCells/admin-cells'; import AdminSettings from './Components/Admin-Settings/AdminSettings'; import ColaboradoresInscritos from './Components/Colaboradores-Permanentes/Colaboradores'; @@ -290,9 +290,10 @@ function AppInner() {
{!isAdmin && user?.mail && ( <> -
- +
+

Reservar Parqueadero

+ )} diff --git a/src/Components/AdminCells/admin-cells.tsx b/src/Components/AdminCells/admin-cells.tsx index d3624853..ea963edc 100644 --- a/src/Components/AdminCells/admin-cells.tsx +++ b/src/Components/AdminCells/admin-cells.tsx @@ -14,13 +14,13 @@ import ReservationsDisabledNotice, { RESERVATIONS_DISABLED } from '../Notices/Re const AdminCells: React.FC = () => { const { // estado base - error, - rows, pageIndex, hasNext, + error,rows, pageIndex, hasNext, // filtros y paginación search, setSearch, onSearchEnter, tipo, setTipo, itinerancia, setItinerancia, + estado, setEstado, pageSize, setPageSize, nextPage, prevPage, @@ -413,6 +413,19 @@ async function submitQuickReserve() {
+
+ Estado + +
+
{ setTipo: (_: 'all' | 'Carro' | 'Moto') => {}, itinerancia: 'all' as const, setItinerancia: (_: 'all' | 'Empleado Fijo' | 'Empleado Itinerante' | 'Directivo') => {}, + estado: 'Activa' as const, + setEstado: (_: 'all' | 'Activa' | 'Inactiva') => {}, pageSize: 50, setPageSize: (_: number) => {}, pageIndex: 0, @@ -175,6 +177,7 @@ export const useAdminCells = () => { search: c.search, setSearch: c.setSearch, onSearchEnter, tipo: c.tipo, setTipo: c.setTipo, itinerancia: c.itinerancia, setItinerancia: c.setItinerancia, + estado: c.estado, setEstado: c.setEstado, pageSize: c.pageSize, setPageSize: c.setPageSize, pageIndex: c.pageIndex, hasNext: c.hasNext, nextPage: c.nextPage, prevPage: c.prevPage, diff --git a/src/Components/Notices/ReservationsDisabledNotice.tsx b/src/Components/Notices/ReservationsDisabledNotice.tsx index 1a78663e..1deea3a2 100644 --- a/src/Components/Notices/ReservationsDisabledNotice.tsx +++ b/src/Components/Notices/ReservationsDisabledNotice.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -export const RESERVATIONS_DISABLED = true; +export const RESERVATIONS_DISABLED = false; export const RESERVATIONS_DISABLED_SINCE = '4 de septiembre'; type Props = { compact?: boolean }; diff --git a/src/Hooks/useCeldas.ts b/src/Hooks/useCeldas.ts index 728af809..44d96f45 100644 --- a/src/Hooks/useCeldas.ts +++ b/src/Hooks/useCeldas.ts @@ -26,6 +26,9 @@ export type UseParkingSlotsReturn = { itinerancia: 'all' | 'Empleado Fijo' | 'Empleado Itinerante' | 'Directivo'; setItinerancia: (i: 'all' | 'Empleado Fijo' | 'Empleado Itinerante' | 'Directivo') => void; + estado: 'all' | 'Activa' | 'Inactiva'; + setEstado: (e: 'all' | 'Activa' | 'Inactiva') => void; + pageSize: number; setPageSize: (n: number) => void; pageIndex: number; @@ -56,6 +59,7 @@ export function useCeldas(svc: ParkingSlotsService): UseParkingSlotsReturn { const [search, setSearch] = React.useState(''); const [tipo, setTipo] = React.useState<'all' | 'Carro' | 'Moto'>('all'); const [itinerancia, setItinerancia] = React.useState<'all' | 'Empleado Fijo' | 'Empleado Itinerante' | 'Directivo'>('Empleado Itinerante'); + const [estado, setEstado] = React.useState<'all' | 'Activa' | 'Inactiva'>('Activa'); const [pageSize, _setPageSize] = React.useState(50); const [pageIndex, setPageIndex] = React.useState(0); @@ -171,10 +175,20 @@ export function useCeldas(svc: ParkingSlotsService): UseParkingSlotsReturn { : items; const ui = itemsFiltered.map(mapSlotToUI); - setAllRows(ui); - setRows(ui.slice(0, pageSize)); + + // filtro por estado (activa/inactiva) en cliente + const isActiva = (r: SlotUI) => r.Activa === 'Activa'; + const uiPorEstado = estado === 'all' + ? ui + : ui.filter(r => (estado === 'Activa') === isActiva(r)); + + // activas primero, luego inactivas + const uiOrdenado = [...uiPorEstado].sort((a, b) => Number(isActiva(b)) - Number(isActiva(a))); + + setAllRows(uiOrdenado); + setRows(uiOrdenado.slice(0, pageSize)); setPageIndex(0); - setHasNext(ui.length > pageSize); + setHasNext(uiOrdenado.length > pageSize); } catch (e: any) { if (myId !== reqIdRef.current) return; console.error('[useCeldas] reloadAll error:', e); @@ -184,7 +198,7 @@ export function useCeldas(svc: ParkingSlotsService): UseParkingSlotsReturn { } finally { if (myId === reqIdRef.current) setLoading(false); } - }, [svc, pageSize, tipo, itinerancia]); + }, [svc, pageSize, tipo, itinerancia, estado]); // -------- paginación ---------- const setPageSize = React.useCallback((n: number) => { @@ -236,7 +250,7 @@ export function useCeldas(svc: ParkingSlotsService): UseParkingSlotsReturn { return () => { if (debounceRef.current) window.clearTimeout(debounceRef.current); }; - }, [search, tipo, itinerancia, reloadAll]); + }, [search, tipo, itinerancia, estado, reloadAll]); // Carga inicial React.useEffect(() => { @@ -258,6 +272,9 @@ export function useCeldas(svc: ParkingSlotsService): UseParkingSlotsReturn { itinerancia, setItinerancia, + estado, + setEstado, + pageSize, setPageSize, pageIndex,