From f2b2583e62bf43f30de7a081d71afb5c066f134d Mon Sep 17 00:00:00 2001 From: javier Date: Thu, 18 Jun 2026 16:26:54 +0200 Subject: [PATCH] =?UTF-8?q?docs(mobile):=20brief=20de=20traspaso=20para=20?= =?UTF-8?q?la=20app=20m=C3=B3vil=20(endpoints,=20payloads,=20modelo=20de?= =?UTF-8?q?=20datos,=20arquitectura=20offline)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/MOBILE_APP_BRIEF.md | 212 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 docs/MOBILE_APP_BRIEF.md diff --git a/docs/MOBILE_APP_BRIEF.md b/docs/MOBILE_APP_BRIEF.md new file mode 100644 index 0000000..5808738 --- /dev/null +++ b/docs/MOBILE_APP_BRIEF.md @@ -0,0 +1,212 @@ +# ConstruProgress — Brief para la App Móvil + +Documento único de traspaso para construir la app móvil que consume la API de +ConstruProgress. La **fuente de verdad** del contrato es [`openapi.yaml`](openapi.yaml); +el modelo offline está en [`MOBILE_SYNC_PROTOCOL.md`](MOBILE_SYNC_PROTOCOL.md). Este +brief los resume y añade ejemplos de payloads reales y el modelo de datos. + +> Para trabajar en el repo móvil con Claude Code viendo este backend: +> `claude --add-dir C:\xampp\htdocs\construprogress` + +--- + +## 1. Objetivo + +App de **seguimiento de obra** que funciona **sin conexión** en campo: descarga los +datos de un proyecto (estructura + plantillas), permite trabajar offline (actualizar +progreso, registrar inspecciones, gestionar incidencias con tareas/comentarios/fotos) +y **sincroniza cuando hay red**. + +## 2. Autenticación (Laravel Sanctum) + +- Token Bearer **por dispositivo**, con ability `mobile-sync`. +- `POST /login` con `{ email, password, device_name, app_version? }` → `{ token, user }`. +- En el resto de llamadas: cabecera `Authorization: Bearer `. +- `POST /logout` revoca el token del dispositivo actual. +- Guarda el token en almacenamiento seguro (Expo SecureStore / flutter_secure_storage). + +**Base URL:** `https:///api/v1` (confirmar host de despliegue; en local XAMPP +suele ser `http://localhost/construprogress/public/api/v1`). + +```bash +curl -X POST https:///api/v1/login \ + -H "Content-Type: application/json" \ + -d '{"email":"user@mai.group","password":"secret","device_name":"Pixel-8"}' +# → { "token": "12|abc...", "user": { "id":1, "name":"...", "roles":[...], "permissions":[...] } } +``` + +## 3. Endpoints (8) + +| Método | Ruta | Uso | Rate limit | +|---|---|---|---| +| POST | `/login` | Token de dispositivo | 10/min | +| GET | `/me` | Usuario + permisos | — | +| POST | `/logout` | Revocar token | — | +| GET | `/projects` | Proyectos accesibles | — | +| GET | `/projects/{id}/bundle?since=` | PULL: snapshot o delta + tombstones | — | +| GET | `/templates?since=` | Plantillas de inspección (version+hash) | — | +| POST | `/sync` | PUSH: lote de mutaciones offline | 60/min | +| POST | `/media` | Subir fichero (multipart) | 120/min | + +## 4. PULL — descarga de datos + +### 4.1 Primera sincronización (snapshot completo) +`GET /projects/{id}/bundle` devuelve: + +```jsonc +{ + "server_time": "2026-06-18T12:00:00+00:00", // úsalo como próximo `since` + "project": { ... }, + "phases": [ ... ], + "layers": [ ... ], + "features": [ ... ], + "inspections": [ ... ], + "issues": [ ... ], + "issue_tasks": [ ... ], + "issue_comments": [ ... ], + "templates": [ ... ], + "media": [ ... ], + "deleted": {} // vacío en snapshot completo +} +``` + +### 4.2 Sincronizaciones siguientes (delta) +`GET /projects/{id}/bundle?since=` → solo lo cambiado tras `since` +y un objeto `deleted` con los **ids borrados** (tombstones) por entidad: + +```jsonc +"deleted": { + "phases": [], "layers": [], "features": [], "inspections": [], + "issues": [], "issue_tasks": [], "issue_comments": [] +} +``` + +> ⚠️ **URL-encodea el `since`** (el `+` del offset horario). Guarda `server_time` de +> cada respuesta y úsalo como el siguiente `since`. + +### 4.3 Plantillas +`GET /templates?since=` devuelve las plantillas de inspección de los proyectos +accesibles, cada una con `version` (timestamp) y `hash` (para detectar cambios). + +## 5. PUSH — `POST /sync` + +Envía un lote. Cada operación lleva una **`uuid` generada en el cliente** (clave de +idempotencia) y `client_updated_at`: + +```jsonc +{ + "operations": [ + { + "entity": "feature", + "op": "update", + "uuid": "0f8e2b6c-....", // único y estable por operación + "client_updated_at": "2026-06-18T11:30:00+00:00", + "data": { "id": 5, "status": "completed", "progress": 100 } + } + ] +} +``` + +Respuesta — **un resultado por operación**: +```jsonc +{ "results": [ + { "uuid": "0f8e...", "status": "applied", "server_id": 5 } +] } +``` +`status` ∈ `applied | duplicate | conflict | error`. +- `duplicate`: ya se había aplicado esa `uuid` (reintento seguro). +- `conflict`: el servidor es más nuevo → trae `"server": {...}` con el valor actual + (last-write-wins por `client_updated_at`). Resuélvelo en el cliente y reintenta. +- `error`: trae `"error": "..."` (validación o permisos). + +### 5.1 Operaciones soportadas (entity.op → data → permiso requerido) + +| entity.op | `data` | Permiso | +|---|---|---| +| `progress_update.create` | `{ phase_id, progress(0-100), comment?, location? }` | `update progress` | +| `feature.update` | `{ id, status?, progress?(0-100), responsible? }` | `update progress` | +| `inspection.create` | `{ feature_id, template_id?, data?, status?, result?, notes? }` | `create inspections` | +| `issue.create` | `{ project_id, feature_id?, title, description?, priority?, status?, type? }` | `create issues` | +| `issue.update` | `{ id, title?, description?, priority?, status?, type?, assigned_to?, resolution_notes? }` | `edit issues` | +| `issue_task.create` | `{ issue_id, title, assigned_to?, due_date?, is_done? }` | `edit issues` | +| `issue_task.update` | `{ id, title?, assigned_to?, due_date?, is_done? }` | `edit issues` | +| `issue_comment.create` | `{ issue_id, body }` | `view issues` | + +Valores enum: +- `issue.priority`: `low | medium | high | critical` +- `issue.status`: `open | in_review | resolved | closed` +- `issue.type`: `defect | safety | quality | documentation | other` + +> El servidor SIEMPRE fija `user_id`/`reported_by`/`project_id` y valida permiso + +> pertenencia al proyecto. El cliente nunca los manda. + +## 6. Media — `POST /media` (multipart/form-data) + +Campos: `uuid` (idempotencia), `parent_entity`, `parent_id`, `file`, `category?` +(`image|document|other`), `description?`. + +`parent_entity` ∈ `feature | issue | issue_task | issue_comment | project | phase | layer`. + +```bash +curl -X POST https:///api/v1/media \ + -H "Authorization: Bearer " \ + -F "uuid=4b1f...-uuid" \ + -F "parent_entity=issue" -F "parent_id=12" \ + -F "category=image" -F "file=@/path/defecto.jpg" +# → { "status":"applied", "media": { "id":99, "url":"/storage/...", ... } } +``` +Requiere permiso `upload media` + pertenencia al proyecto. Idempotente por `uuid`. + +## 7. Modelo de datos (campos que devuelve el bundle) + +``` +project : id, reference, name, address, lat, lng, status, updated_at +phase : id, name, order, color, progress_percent, updated_at +layer : id, phase_id, name, color, updated_at +feature : id, layer_id, name, geometry(GeoJSON), status, progress, + responsible, template_id, updated_at +inspection : id, feature_id, layer_id, template_id, user_id, data(obj), + status, result, notes, created_at, updated_at +issue : id, feature_id, title, description, status, priority, type, + reported_by, assigned_to, resolved_at, updated_at +issue_task : id, issue_id, title, is_done, done_at, done_by, assigned_to, + due_date, order, updated_at +issue_comment : id, issue_id, user_id, body, created_at, updated_at +template : id, project_id, phase_id, name, description, fields(array), + version, hash, updated_at +media : id, uuid, parent_entity, parent_id, url, name, file_type, + category, updated_at +``` + +## 8. Arquitectura cliente recomendada + +**Stack:** React Native + Expo (alternativa: Flutter). Offline-first: + +1. **BD local**: SQLite (expo-sqlite / WatermelonDB) — o Drift/Isar en Flutter. + Refleja las entidades del bundle. +2. **Sincronización PULL**: guarda `server_time`; en cada arranque/con red llama a + `bundle?since=<último server_time>`, aplica upserts y borra los `deleted`. +3. **Outbox (cola de salida)**: cada cambio offline genera una operación con `uuid` + propio y se encola. Con red, envías el lote a `/sync` y procesas los resultados: + `applied/duplicate` → marcar enviado; `conflict` → re-mergear; `error` → revisar. +4. **Media**: sube los ficheros pendientes a `/media` (también con `uuid`) y referencia + la `url` devuelta. +5. **Token**: en almacenamiento seguro; si 401 → re-login. + +### Flujo típico de sesión +``` +login → guardar token +GET /projects → elegir proyecto +GET /projects/{id}/bundle (sin since) → poblar BD local +... trabajo offline (encolar operaciones + fotos) ... +con red: POST /sync (lote) → POST /media (ficheros) → GET bundle?since=server_time +``` + +## 9. Checklist de arranque del repo móvil +- [ ] Elegir stack (RN+Expo / Flutter) y crear el proyecto. +- [ ] `claude --add-dir C:\xampp\htdocs\construprogress` para tener el contrato a mano. +- [ ] Capa de API (login/me/logout, projects, bundle, templates, sync, media). +- [ ] BD local + repositorios por entidad. +- [ ] Motor de sync (PULL delta + outbox PUSH + media) con manejo de conflictos. +- [ ] UI: lista de proyectos, mapa/fases, inspecciones, incidencias (checklist, + comentarios, fotos), indicador de estado de sincronización.