Files
construprogress/docs/MOBILE_APP_BRIEF.md
T

8.9 KiB

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; el modelo offline está en 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 <token>.
  • POST /logout revoca el token del dispositivo actual.
  • Guarda el token en almacenamiento seguro (Expo SecureStore / flutter_secure_storage).

Base URL: https://<host>/api/v1 (confirmar host de despliegue; en local XAMPP suele ser http://localhost/construprogress/public/api/v1).

curl -X POST https://<host>/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:

{
  "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=<ISO8601 URL-encoded> → solo lo cambiado tras since y un objeto deleted con los ids borrados (tombstones) por entidad:

"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:

{
  "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:

{ "results": [
  { "uuid": "0f8e...", "status": "applied", "server_id": 5 }
] }

statusapplied | 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_entityfeature | issue | issue_task | issue_comment | project | phase | layer.

curl -X POST https://<host>/api/v1/media \
  -H "Authorization: Bearer <token>" \
  -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.