- 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>
235 lines
10 KiB
Markdown
235 lines
10 KiB
Markdown
# 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`](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 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 /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).
|
|
|
|
```bash
|
|
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:
|
|
|
|
```jsonc
|
|
{
|
|
"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:
|
|
|
|
```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 (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_id` se **eliminó**. `template.project_id` sigue 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`:
|
|
|
|
```jsonc
|
|
{
|
|
"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**:
|
|
```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 |
|
|
|---|---|---|
|
|
| `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 | 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://<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:
|
|
|
|
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.
|
|
|
|
### 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\construprogress` para 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.
|