- openapi.yaml: bumped a 1.1.0, añadido changelog con los cambios acumulados respecto al brief inicial; /templates y bundle.templates documentan que ahora usan pivot (asignación por proyecto); enums de issue documentados. - MOBILE_APP_BRIEF.md: reescrito para reflejar v1.1 — bundle con feature_types / issue_tasks / issue_comments; feature con feature_type_id/is_active; issue.type; nuevas ops (issue.update, issue_task.*, issue_comment.create); parent_entity admite issue_task/issue_comment; deleted incluye tasks/comments; y bloque detallado sobre templates ahora globales con fields[].group/question/help. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10 KiB
ConstruProgress — Brief para la App Móvil
Contrato de la API móvil (v1.1) — actualizado 2026-07-07.
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 asignadas), permite trabajar offline (actualizar progreso, registrar inspecciones con fotos, 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 /logincon{ email, password, device_name, app_version? }→{ token, user }.- En el resto de llamadas: cabecera
Authorization: Bearer <token>. POST /logoutrevoca 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).
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 asignadas a proyectos accesibles | — |
| POST | /sync |
PUSH: lote de mutaciones offline | 60/min |
| POST | /media |
Subida de ficheros (multipart) | 120/min |
4. PULL — descarga de datos
4.1 Primera sincronización (snapshot completo)
GET /projects/{id}/bundle devuelve:
{
"server_time": "2026-07-07T12:00:00+00:00", // úsalo como próximo `since`
"project": { ... },
"phases": [ ... ],
"layers": [ ... ],
"features": [ ... ],
"feature_types": [ ... ], // catálogo global (id, name, color)
"inspections": [ ... ],
"issues": [ ... ],
"issue_tasks": [ ... ], // checklist por incidencia
"issue_comments": [ ... ], // comentarios por incidencia
"templates": [ ... ], // asignadas al proyecto (vía pivot)
"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). Guardaserver_timede cada respuesta y úsalo como el siguientesince.
4.3 Plantillas (globales, con asignación por proyecto)
GET /templates?since= devuelve las plantillas del catálogo global que están
asignadas a alguno de los proyectos accesibles del usuario (vía pivot
inspection_template_project). Cada plantilla trae version (timestamp) y hash.
Cambio de v1.0 → v1.1: las plantillas ya no viven por proyecto. Son un catálogo global, y los proyectos consumen las que se les han asignado.
template.phase_idse eliminó.template.project_idsigue en el JSON por compatibilidad pero no determina visibilidad — trátalo como informativo.
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-07-07T11: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 }
] }
status ∈ applied | duplicate | conflict | error.
duplicate: ya se había aplicado esauuid(reintento seguro).conflict: el servidor es más nuevo → trae"server": {...}con el valor actual (last-write-wins porclient_updated_at). Resuélvelo en el cliente y reintenta.error: trae"error": "..."(validación o permisos).
5.1 Operaciones soportadas
| entity.op | data |
Permiso |
|---|---|---|
progress_update.create |
{ phase_id, progress(0-100), comment?, location? } |
update progress |
feature.update |
{ id, status?, progress?(0-100), responsible?, is_active?, feature_type_id? } |
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 | criticalissue.status:open | in_review | resolved | closedissue.type:defect | safety | quality | documentation | other
El servidor SIEMPRE fija
user_id/reported_by/project_idy 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.
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, feature_type_id, is_active, updated_at
feature_type : id, name, color # catálogo global
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, name, description, fields(array), version, hash,
project_id(informativo, puede ser null), updated_at
# fields[]: cada campo puede incluir:
# group (string, sección para agrupar)
# question (string, prompt corto en la inspección)
# help (string, ayuda/comentarios largos)
# name, label, type, required, options, min, max, step
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:
- BD local: SQLite (expo-sqlite / WatermelonDB) — o Drift/Isar en Flutter. Refleja las entidades del bundle.
- Sincronización PULL: guarda
server_time; en cada arranque/con red llama abundle?since=<último server_time>, aplica upserts y borra losdeleted. - Outbox (cola de salida): cada cambio offline genera una operación con
uuidpropio y se encola. Con red, envías el lote a/syncy procesas los resultados:applied/duplicate→ marcar enviado;conflict→ re-mergear;error→ revisar. - Media: sube los ficheros pendientes a
/media(también conuuid) y referencia laurldevuelta. - Token: en almacenamiento seguro; si 401 → re-login.
Renderizar plantillas de inspección
Cada plantilla trae fields[] con posibles metadatos: group (agrupar campos por
sección), question (mostrar como prompt), help (bajo el campo). El resto de
campos (name, label, type, required, options, min, max, step) sigue
igual. Al enviar la inspección (inspection.create), data es un objeto plano
{ nombre_del_campo: valor } — los grupos son solo de presentación.
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\construprogresspara tener el contrato a mano.- Capa de API (login/me/logout, projects, bundle, templates, sync, media).
- BD local + repositorios por entidad (incluyendo feature_types, issue_tasks,
issue_comments, y templates con
fields[].group/question/help). - Motor de sync (PULL delta + outbox PUSH + media) con manejo de conflictos.
- UI: lista de proyectos, mapa/fases, inspecciones (grupos + fotos), incidencias (checklist, comentarios, fotos), indicador de estado de sincronización.