Prototipo YB01 #1

Open
edgar.friendly wants to merge 16 commits from prototipo-yb01 into master
10 changed files with 213 additions and 0 deletions
Showing only changes of commit 7bb3ea22da - Show all commits
@@ -0,0 +1,40 @@
# Botón flotante para proponer locales
## Purpose
Punto de entrada único y siempre visible para proponer un nuevo local al directorio: un botón de acción flotante (FAB) fijo en la esquina inferior derecha, usable en PC y móvil, con tooltip explicativo y apertura de la pasarela de propuesta al pulsarlo.
## Requirements
### Requirement: Botón flotante fijo en la esquina inferior derecha
El sistema SHALL mostrar un botón de acción flotante (FAB) fijado en la esquina inferior derecha de la ventana, siempre visible sobre el contenido, tanto en PC como en móvil.
#### Scenario: Visible en escritorio
- **WHEN** la página se carga en una pantalla de escritorio
- **THEN** el botón flotante aparece fijado en la esquina inferior derecha, por encima del resto del contenido
#### Scenario: Visible en móvil
- **WHEN** la página se carga o se hace scroll en un dispositivo móvil
- **THEN** el botón flotante permanece fijo y accesible en la esquina inferior derecha, sin quedar oculto por el contenido ni interferir con la barra inferior existente
#### Scenario: Reemplazo del botón de cabecera
- **WHEN** la página se carga
- **THEN** ya no existe el botón "Proponer local" en la cabecera y el único punto de entrada para proponer un local es el botón flotante
### Requirement: Tooltip explicativo al pasar el ratón
El botón flotante SHALL mostrar un tooltip al pasar el ratón por encima (hover) y al recibir foco por teclado, explicando que sirve para añadir un nuevo local.
#### Scenario: Tooltip en hover
- **WHEN** el usuario pasa el ratón por encima del botón flotante
- **THEN** aparece un tooltip con un texto explicativo del tipo "Añade un nuevo local al directorio"
#### Scenario: Tooltip accesible sin ratón
- **WHEN** el botón flotante recibe el foco por teclado o se consulta con un lector de pantalla
- **THEN** el botón expone su propósito mediante `aria-label`, de modo que la función sea accesible sin depender del hover
### Requirement: El botón abre la pasarela en un modal
El botón flotante SHALL abrir, al pulsarlo, la pasarela de propuesta de local en un modal por encima de la página.
#### Scenario: Apertura de la pasarela
- **WHEN** el usuario pulsa el botón flotante
- **THEN** se abre un modal con la pasarela de propuesta, comenzando por la primera pantalla (Ubicación)
@@ -0,0 +1,96 @@
# Pasarela de propuesta de local
## Purpose
Experiencia guiada (wizard) en un modal para proponer un local con un solo campo por pantalla —empezando por la ubicación con autodetección del proveedor e inferencia de nombre/provincia—, sin campos de comentario ni puntuación, conservando las validaciones existentes (palabras prohibidas, duplicados, privacidad obligatoria).
## Requirements
### Requirement: Pasarela paso a paso en un modal
El sistema SHALL presentar la propuesta de un local como una pasarela (wizard) dentro de un modal, con un solo campo por pantalla, navegación atrás/siguiente e indicador de progreso.
#### Scenario: Una pantalla por campo
- **WHEN** la pasarela está abierta
- **THEN** cada pantalla muestra únicamente el campo correspondiente al paso actual, con botones para avanzar y retroceder
#### Scenario: Navegación hacia atrás
- **WHEN** el usuario pulsa "Atrás" en una pantalla intermedia
- **THEN** vuelve a la pantalla anterior conservando los datos ya introducidos
#### Scenario: Avance bloqueado sin dato válido
- **WHEN** el campo de la pantalla actual está vacío o es inválido
- **THEN** el botón "Siguiente" está deshabilitado y la pantalla no avanza
#### Scenario: Cierre del modal
- **WHEN** el usuario cierra el modal (botón ✕, botón Cancelar o tecla Escape)
- **THEN** la pasarela se cierra y se descarta el borrador en curso
### Requirement: Orden de las pantallas empezando por Ubicación
La pasarela SHALL presentar las pantallas en este orden: 1) Ubicación, 2) Nombre, 3) Provincia, 4) Categoría, 5) Tipo específico (opcional), 6) Revisión y envío.
#### Scenario: Primera pantalla
- **WHEN** se abre la pasarela
- **THEN** la primera pantalla que se muestra es la de Ubicación
#### Scenario: Tipo específico omitible
- **WHEN** el usuario está en la pantalla de Tipo específico
- **THEN** puede continuar sin seleccionar ninguna subcategoría
### Requirement: Inferencia de Nombre y Provincia desde la Ubicación
El sistema SHALL pre-rellenar los campos Nombre y Provincia a partir de la ubicación introducida en la primera pantalla (geocodificación inversa), permitiendo al usuario corregirlos.
#### Scenario: Nombre y provincia pre-rellenados
- **WHEN** el usuario introduce una ubicación válida en la primera pantalla y esta se resuelve con éxito
- **THEN** al llegar a las pantallas de Nombre y Provincia ambos campos aparecen pre-rellenados con los valores inferidos
#### Scenario: Valores corregibles
- **WHEN** los valores inferidos no son correctos
- **THEN** el usuario puede editarlos libremente antes de enviar
#### Scenario: Degradación sin inferencia
- **WHEN** la inferencia falla o devuelve campos vacíos
- **THEN** las pantallas de Nombre y Provincia aparecen vacías y el usuario los rellena manualmente, sin bloquear la pasarela
### Requirement: Sin campo de comentario ni puntuación
La pasarela SHALL NOT incluir el campo de comentario/descripción ni el campo de puntuación; la propuesta se envía sin esos datos.
#### Scenario: Envío sin comentario ni puntuación
- **WHEN** el usuario completa la pasarela y envía
- **THEN** la propuesta se registra con nombre, provincia, categoría, subcategoría, dirección/enlace y coordenadas, sin descripción y sin puntuación
### Requirement: Revisión y aceptación de privacidad antes de enviar
La última pantalla de la pasarela SHALL mostrar un resumen de los datos introducidos y el checkbox de aceptación de la política de privacidad, siendo este último obligatorio para enviar.
#### Scenario: Envío bloqueado sin aceptar privacidad
- **WHEN** el usuario llega a la pantalla final sin marcar la casilla de privacidad
- **THEN** el botón de envío está deshabilitado hasta que la marque
#### Scenario: Resumen antes de enviar
- **WHEN** el usuario llega a la pantalla final
- **THEN** ve un resumen legible de todos los datos introducidos con posibilidad de volver atrás a corregirlos
### Requirement: Validaciones existentes integradas en la pasarela
La pasarela SHALL conservar las validaciones actuales del formulario: filtro de palabras prohibidas y comprobación de duplicados, mostrando los avisos dentro del modal.
#### Scenario: Palabra prohibida detectada
- **WHEN** el texto introducido contiene una palabra no permitida
- **THEN** se muestra un aviso dentro del modal y la propuesta no se envía
#### Scenario: Posible duplicado detectado
- **WHEN** al enviar se detecta un local igual o muy cercano
- **THEN** se muestra el aviso de duplicado dentro del modal con las coincidencias y las opciones de enviar de todos modos o revisar los datos
#### Scenario: Envío correcto
- **WHEN** la propuesta se envía con éxito
- **THEN** el modal se cierra y aparece la confirmación de propuesta enviada en la página principal
### Requirement: Modal responsive accesible
El modal de la pasarela SHALL ser usable en PC (diálogo centrado) y móvil (ocupando prácticamente toda la pantalla), con foco contenido y cierre mediante Escape.
#### Scenario: Uso en móvil
- **WHEN** la pasarela se abre en una pantalla estrecha
- **THEN** el modal ocupa el ancho completo y es operativo con teclado y scroll táctil
#### Scenario: Foco contenido en el modal
- **WHEN** el modal está abierto
- **THEN** el foco de teclado permanece dentro del modal y la tecla Escape lo cierra
@@ -0,0 +1,77 @@
# Proveedores de ubicación (integraciones extensible)
## Purpose
Sistema extensible de integraciones con backends de ubicación (Google Maps, Waze, OpenStreetMap, coordenadas sueltas y geocodificación Nominatim) bajo una interfaz común (`id`/`detecta`/`extrae`), capaz de autodetectar el proveedor de una entrada libre, extraer coordenadas e inferir nombre y provincia, y ampliable a otras extracciones futuras sin modificar los proveedores existentes.
## Requirements
### Requirement: Interfaz común para proveedores de ubicación
Cada integración con un backend de ubicación SHALL implementarse como una clase que cumpla una misma interfaz: identificarse (`id`), indicar si reconoce una entrada (`detecta(entrada)`) y extraer la información disponible (`extrae(entrada)`).
#### Scenario: Contrato uniforme
- **WHEN** se consulta cualquier proveedor registrado
- **THEN** expone los métodos `detecta(entrada)` y `extrae(entrada)` con la misma firma y semántica, de modo que el consumidor no necesita conocer la clase concreta
### Requirement: Resultado de extracción extensible
El método `extrae(entrada)` SHALL devolver un objeto de resultado extensible que incluya como mínimo las coordenadas (`lat`, `lng`) y el identificador del proveedor que las extrajo, pudiendo incorporar campos adicionales en el futuro sin romper a los consumidores.
#### Scenario: Extracción mínima
- **WHEN** un proveedor extrae una ubicación válida
- **THEN** el resultado contiene `lat`, `lng` y el identificador del proveedor
#### Scenario: Campos adicionales sin ruptura
- **WHEN** en el futuro un proveedor añade nuevos campos al resultado (horarios, teléfono, fotos…)
- **THEN** los consumidores existentes siguen funcionando sin modificación, ignorando los campos que no conocen
### Requirement: Autodetección del proveedor a partir de la entrada
El sistema SHALL autodetectar automáticamente el proveedor adecuado para una entrada dada, recorriendo los proveedores registrados y usando el primero que la reconozca.
#### Scenario: Enlace de Google Maps
- **WHEN** el usuario pega un enlace de Google Maps (formatos `@lat,lng`, `!3d…!4d…` o `?q=lat,lng`)
- **THEN** el sistema lo resuelve con el proveedor de Google Maps y obtiene las coordenadas
#### Scenario: Enlace de Waze
- **WHEN** el usuario pega un enlace de Waze (`waze.com/ul`, parámetro `ll=lat,lng` o búsqueda `q=`)
- **THEN** el sistema lo resuelve con el proveedor de Waze y obtiene las coordenadas
#### Scenario: Enlace de OpenStreetMap
- **WHEN** el usuario pega un enlace de openstreetmap.org (parámetros `mlat`/`mlon` o fragmento `#map=zoom/lat/lng`)
- **THEN** el sistema lo resuelve con el proveedor de OpenStreetMap y obtiene las coordenadas
#### Scenario: Coordenadas geográficas sueltas
- **WHEN** el usuario escribe coordenadas directamente (por ejemplo `41.3851, 2.1734`)
- **THEN** el sistema lo resuelve con el proveedor de coordenadas y obtiene lat/lng
#### Scenario: Texto de dirección como fallback
- **WHEN** la entrada no coincide con ningún enlace soportado y parece una dirección en texto
- **THEN** el sistema la resuelve con el proveedor de geocodificación (Nominatim) realizando una búsqueda por texto
#### Scenario: Entrada no reconocida
- **WHEN** la entrada no es reconocida por ningún proveedor ni resuelta como dirección
- **THEN** el sistema informa de que no se pudo interpretar la ubicación y no avanza de pantalla
### Requirement: Inferencia de nombre y provincia
Los proveedores SHALL extraer, cuando estén disponibles, los campos inferibles `nombre` y `provincia` a partir de la entrada resuelta (geocodificación inversa cuando solo hay coordenadas), normalizando la provincia al listado canónico de provincias de España.
#### Scenario: Inferencia desde coordenadas
- **WHEN** la entrada se resuelve a coordenadas sin más contexto
- **THEN** una geocodificación inversa obtiene un nombre descriptivo y la provincia canónica (coincidencia insensible a acentos y mayúsculas con el listado de PROVINCIAS)
#### Scenario: Provincia fuera del listado
- **WHEN** la provincia inferida no coincide con ninguna provincia del listado canónico
- **THEN** el campo provincia queda vacío para que el usuario lo seleccione manualmente
### Requirement: Extensibilidad sin modificar proveedores existentes
Añadir un proveedor nuevo SHALL consistir en crear una nueva clase que implemente la interfaz y registrarla en el resolver, sin modificar las clases existentes ni la lógica de la pasarela (principio abierto/cerrado).
#### Scenario: Registro de un proveedor nuevo
- **WHEN** se añade una clase `MiProveedor` con `detecta`/`extrae` y se registra en la lista de proveedores
- **THEN** el campo universal empieza a aceptar las entradas que ese proveedor reconoce sin ningún otro cambio de código
### Requirement: Errores de red degradados
Los proveedores SHALL tratar sus errores de red o de servicio devolviendo un fallo controlado, sin romper la pasarela ni propagar excepciones al consumidor.
#### Scenario: Fallo del servicio de geocodificación
- **WHEN** Nominatim no responde o devuelve un error
- **THEN** la pantalla de Ubicación muestra un aviso de error y el usuario puede reintentar o continuar sin inferencia