diff --git a/openspec/changes/pasarela-proponer-local/.openspec.yaml b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/.openspec.yaml similarity index 100% rename from openspec/changes/pasarela-proponer-local/.openspec.yaml rename to openspec/changes/archive/2026-08-16-pasarela-proponer-local/.openspec.yaml diff --git a/openspec/changes/pasarela-proponer-local/design.md b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/design.md similarity index 100% rename from openspec/changes/pasarela-proponer-local/design.md rename to openspec/changes/archive/2026-08-16-pasarela-proponer-local/design.md diff --git a/openspec/changes/pasarela-proponer-local/proposal.md b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/proposal.md similarity index 100% rename from openspec/changes/pasarela-proponer-local/proposal.md rename to openspec/changes/archive/2026-08-16-pasarela-proponer-local/proposal.md diff --git a/openspec/changes/pasarela-proponer-local/specs/boton-flotante-proponer-local/spec.md b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/boton-flotante-proponer-local/spec.md similarity index 100% rename from openspec/changes/pasarela-proponer-local/specs/boton-flotante-proponer-local/spec.md rename to openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/boton-flotante-proponer-local/spec.md diff --git a/openspec/changes/pasarela-proponer-local/specs/pasarela-proponer-local/spec.md b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/pasarela-proponer-local/spec.md similarity index 100% rename from openspec/changes/pasarela-proponer-local/specs/pasarela-proponer-local/spec.md rename to openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/pasarela-proponer-local/spec.md diff --git a/openspec/changes/pasarela-proponer-local/specs/proveedores-ubicacion/spec.md b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/proveedores-ubicacion/spec.md similarity index 100% rename from openspec/changes/pasarela-proponer-local/specs/proveedores-ubicacion/spec.md rename to openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/proveedores-ubicacion/spec.md diff --git a/openspec/changes/pasarela-proponer-local/tasks.md b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/tasks.md similarity index 100% rename from openspec/changes/pasarela-proponer-local/tasks.md rename to openspec/changes/archive/2026-08-16-pasarela-proponer-local/tasks.md diff --git a/openspec/specs/boton-flotante-proponer-local/spec.md b/openspec/specs/boton-flotante-proponer-local/spec.md new file mode 100644 index 0000000..80d9046 --- /dev/null +++ b/openspec/specs/boton-flotante-proponer-local/spec.md @@ -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) diff --git a/openspec/specs/pasarela-proponer-local/spec.md b/openspec/specs/pasarela-proponer-local/spec.md new file mode 100644 index 0000000..474fd42 --- /dev/null +++ b/openspec/specs/pasarela-proponer-local/spec.md @@ -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 diff --git a/openspec/specs/proveedores-ubicacion/spec.md b/openspec/specs/proveedores-ubicacion/spec.md new file mode 100644 index 0000000..0c57c4a --- /dev/null +++ b/openspec/specs/proveedores-ubicacion/spec.md @@ -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