chore(openspec): archiva pasarela-proponer-local y sincroniza specs principales
- Mueve el cambio (36/36 tareas, 4/4 artefactos) a changes/archive/ - Crea las specs principales de boton-flotante-proponer-local, pasarela-proponer-local y proveedores-ubicacion (con Purpose)
This commit is contained in:
@@ -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
|
||||||
Reference in New Issue
Block a user