From fdf85e7cc96c34b52f205f4cc86ff8042405083e Mon Sep 17 00:00:00 2001 From: javier Date: Tue, 7 Jul 2026 17:39:47 +0200 Subject: [PATCH] feat(api): adaptar cliente al contrato v1.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - feature_types: nueva tabla local + tipo FeatureType + upsert en applyBundle + getFeatureTypes(); FeatureDetail muestra badge del tipo con su color. - feature: columnas feature_type_id + is_active (migración BD v3); feature.update acepta ambas; toggle "Activa" en el detalle con badge "inactiva" cuando corresponde. - templates: ahora catálogo global asignado por pivot — getTemplates() ya no filtra por project_id (informativo); phase_id eliminado del tipo y del pick. - Formulario de inspección: soporta fields[].group (secciones), question (etiqueta principal), help (texto de ayuda) y required (validación al guardar con aviso de campos faltantes). - Bundle: tipa issue_tasks correctamente como IssueTask[]. - docs/: sincronizados openapi.yaml y MOBILE_APP_BRIEF.md v1.1 del backend. Co-Authored-By: Claude Sonnet 4.6 --- docs/MOBILE_APP_BRIEF.md | 70 +++++++---- docs/openapi.yaml | 65 +++++++++- src/api/types.ts | 24 +++- src/db/database.ts | 8 ++ src/db/repositories.ts | 29 ++++- src/db/schema.ts | 11 +- src/screens/InspectionFormScreen.tsx | 132 ++++++++++++++------ src/screens/detail/FeatureDetailContent.tsx | 40 +++++- src/sync/mutations.ts | 10 ++ 9 files changed, 307 insertions(+), 82 deletions(-) diff --git a/docs/MOBILE_APP_BRIEF.md b/docs/MOBILE_APP_BRIEF.md index 5808738..5c35a78 100644 --- a/docs/MOBILE_APP_BRIEF.md +++ b/docs/MOBILE_APP_BRIEF.md @@ -1,5 +1,7 @@ # 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 @@ -13,9 +15,9 @@ brief los resume y añade ejemplos de payloads reales y el modelo de datos. ## 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**. +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) @@ -25,8 +27,7 @@ y **sincroniza cuando hay red**. - `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`). +**Base URL:** `https:///api/v1` (confirmar host de despliegue). ```bash curl -X POST https:///api/v1/login \ @@ -44,9 +45,9 @@ curl -X POST https:///api/v1/login \ | 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) | — | +| GET | `/templates?since=` | Plantillas **asignadas** a proyectos accesibles | — | | POST | `/sync` | PUSH: lote de mutaciones offline | 60/min | -| POST | `/media` | Subir fichero (multipart) | 120/min | +| POST | `/media` | Subida de ficheros (multipart) | 120/min | ## 4. PULL — descarga de datos @@ -55,18 +56,19 @@ curl -X POST https:///api/v1/login \ ```jsonc { - "server_time": "2026-06-18T12:00:00+00:00", // úsalo como próximo `since` + "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": [ ... ], - "issue_comments": [ ... ], - "templates": [ ... ], + "issue_tasks": [ ... ], // checklist por incidencia + "issue_comments": [ ... ], // comentarios por incidencia + "templates": [ ... ], // asignadas al proyecto (vía pivot) "media": [ ... ], - "deleted": {} // vacío en snapshot completo + "deleted": {} // vacío en snapshot completo } ``` @@ -84,9 +86,15 @@ y un objeto `deleted` con los **ids borrados** (tombstones) por entidad: > ⚠️ **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). +### 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` @@ -100,7 +108,7 @@ idempotencia) y `client_updated_at`: "entity": "feature", "op": "update", "uuid": "0f8e2b6c-....", // único y estable por operación - "client_updated_at": "2026-06-18T11:30:00+00:00", + "client_updated_at": "2026-07-07T11:30:00+00:00", "data": { "id": 5, "status": "completed", "progress": 100 } } ] @@ -119,12 +127,12 @@ Respuesta — **un resultado por operación**: (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) +### 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? }` | `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` | @@ -164,7 +172,8 @@ 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 + 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, @@ -172,8 +181,13 @@ issue : id, feature_id, title, description, status, priority, type, 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 +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 ``` @@ -193,6 +207,13 @@ media : id, uuid, parent_entity, parent_id, url, name, file_type, 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 @@ -206,7 +227,8 @@ con red: POST /sync (lote) → POST /media (ficheros) → GET bundle?since=serve - [ ] 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. +- [ ] 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, incidencias (checklist, - comentarios, fotos), indicador de estado de sincronización. +- [ ] UI: lista de proyectos, mapa/fases, inspecciones (grupos + fotos), incidencias + (checklist, comentarios, fotos), indicador de estado de sincronización. diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 30e168f..ea28b08 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,11 +1,37 @@ openapi: 3.0.3 info: title: ConstruProgress Mobile API - version: "1.0.0" + version: "1.1.0" description: > Offline-first sync API for the mobile app. Auth via Laravel Sanctum bearer tokens (ability `mobile-sync`). All protected endpoints require `Authorization: Bearer `. See docs/MOBILE_SYNC_PROTOCOL.md. + + + ## Changelog (from v1.0) + + + - Inspection templates are now **global** and assigned to projects via + pivot. `bundle.templates` and `/templates` return templates *assigned* to + the accessible projects (no longer filtered by `project_id`). + `template.phase_id` was **removed**. `project_id` remains in the schema + for compatibility but no longer determines visibility. + + - Template `fields[]` items may include `group` (section), `question` (short + prompt shown at inspection time) and `help` (long help text). + + - New entities in the bundle: `feature_types` (global catalogue), + `issue_tasks`, `issue_comments`. + + - `feature` gains `feature_type_id`, `is_active`; `issue` gains `type`. + + - New `/sync` ops: `issue.update`, `issue_task.create`, `issue_task.update`, + `issue_comment.create`. `feature.update` accepts `is_active` and + `feature_type_id`; `issue.create`/`update` accept `type`. + + - `POST /media` `parent_entity` also accepts `issue_task` and `issue_comment`. + + - `deleted` tombstones include `issue_tasks` and `issue_comments`. servers: - url: /api/v1 security: @@ -85,7 +111,12 @@ paths: "403": { description: Not a member of the project } /templates: get: - summary: Inspection templates for accessible projects (with version/hash) + summary: Global inspection templates ASSIGNED to any accessible project (via pivot) + description: > + Templates are a global catalogue. This endpoint returns the templates + that have been *assigned* (via `inspection_template_project` pivot) to + any of the user's accessible projects. Each item includes `version` + (updated_at timestamp) and `hash` for change detection. parameters: - name: since in: query @@ -183,11 +214,33 @@ components: phases: { type: array, items: { type: object } } layers: { type: array, items: { type: object } } features: { type: array, items: { type: object } } + feature_types: + type: array + description: Global catalogue of feature types (id, name, color) + items: { type: object } inspections: { type: array, items: { type: object } } - issues: { type: array, items: { type: object } } - issue_tasks: { type: array, items: { type: object } } - issue_comments: { type: array, items: { type: object } } - templates: { type: array, items: { type: object } } + issues: + type: array + description: > + Includes `type` (defect|safety|quality|documentation|other), + `status` (open|in_review|resolved|closed), + `priority` (low|medium|high|critical). + items: { type: object } + issue_tasks: + type: array + description: Checklist tasks per issue. + items: { type: object } + issue_comments: + type: array + description: Comment thread per issue. + items: { type: object } + templates: + type: array + description: > + Templates assigned to this project via pivot (no longer filtered by + project_id). Each item exposes version/hash for change detection and + `fields[]` where each field may include `group`, `question`, `help`. + items: { type: object } media: { type: array, items: { type: object } } deleted: type: object diff --git a/src/api/types.ts b/src/api/types.ts index 61cef2e..3dc9824 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -65,9 +65,19 @@ export interface Feature { progress?: number; responsible?: string | null; template_id?: number | null; + feature_type_id?: number | null; + is_active?: boolean | number; updated_at: string; } +/** Catálogo global de tipos de feature (v1.1). */ +export interface FeatureType { + id: number; + name: string; + color?: string | null; + updated_at?: string; +} + export interface Inspection { id: number; feature_id: number; @@ -122,10 +132,17 @@ export interface IssueComment { updated_at: string; } +/** + * Plantilla de inspección. Desde v1.1 son un catálogo GLOBAL asignado a + * proyectos vía pivot: `project_id` es informativo (puede ser null) y ya NO + * determina visibilidad; `phase_id` se eliminó del contrato. + * Cada item de `fields[]` puede incluir `group` (sección), `question` + * (prompt corto) y `help` (ayuda larga), además de name/label/type/required/ + * options/min/max/step. + */ export interface Template { id: number; - project_id?: number; - phase_id?: number | null; + project_id?: number | null; name: string; description?: string | null; fields?: unknown[]; @@ -172,9 +189,10 @@ export interface Bundle { phases: Phase[]; layers: Layer[]; features: Feature[]; + feature_types?: FeatureType[]; inspections: Inspection[]; issues: Issue[]; - issue_tasks: IssueComment[] | IssueTask[]; + issue_tasks: IssueTask[]; issue_comments: IssueComment[]; templates: Template[]; media: Media[]; diff --git a/src/db/database.ts b/src/db/database.ts index ee5bc39..a04936f 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -28,6 +28,13 @@ async function openAndMigrate(): Promise { await db.execAsync('ALTER TABLE outbox ADD COLUMN local_id INTEGER'); } + if (current < 3) { + // API v1.1: feature gana feature_type_id + is_active. La tabla + // feature_types la crea SCHEMA_SQL (CREATE TABLE IF NOT EXISTS). + await db.execAsync('ALTER TABLE features ADD COLUMN feature_type_id INTEGER'); + await db.execAsync('ALTER TABLE features ADD COLUMN is_active INTEGER DEFAULT 1'); + } + if (current < SCHEMA_VERSION) { await db.execAsync(`PRAGMA user_version = ${SCHEMA_VERSION}`); } @@ -43,6 +50,7 @@ export async function wipeDatabase(): Promise { 'phases', 'layers', 'features', + 'feature_types', 'inspections', 'issues', 'issue_tasks', diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 87b4a4f..cc271b2 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -8,6 +8,7 @@ import { Bundle, DeletedTombstones, Feature, + FeatureType, Inspection, Issue, IssueComment, @@ -102,6 +103,9 @@ export async function applyBundle(bundle: Bundle): Promise { for (const f of bundle.features ?? []) { await upsertById(db, 'features', { ...pickFeature(f), project_id: pid }); } + for (const ft of bundle.feature_types ?? []) { + await upsertById(db, 'feature_types', pickFeatureType(ft)); + } for (const i of bundle.inspections ?? []) { await upsertById(db, 'inspections', { ...pickInspection(i), project_id: pid }); } @@ -202,9 +206,18 @@ const pickFeature = (f: Feature) => ({ progress: f.progress ?? null, responsible: f.responsible ?? null, template_id: f.template_id ?? null, + feature_type_id: f.feature_type_id ?? null, + is_active: f.is_active == null ? 1 : Number(f.is_active), updated_at: f.updated_at, }); +const pickFeatureType = (t: FeatureType) => ({ + id: t.id, + name: t.name, + color: t.color ?? null, + updated_at: t.updated_at ?? null, +}); + const pickInspection = (i: import('../api/types').Inspection) => ({ id: i.id, feature_id: i.feature_id, @@ -258,7 +271,6 @@ const pickIssueComment = (c: IssueComment) => ({ const pickTemplate = (t: Template) => ({ id: t.id, project_id: t.project_id ?? null, - phase_id: t.phase_id ?? null, name: t.name, description: t.description ?? null, fields: json(t.fields), @@ -380,11 +392,15 @@ export async function getIssueComments(issueId: number): Promise ); } -export async function getTemplates(projectId: number): Promise { +/** + * Plantillas locales. Desde API v1.1 son un catálogo global asignado por + * pivot: el bundle solo trae las asignadas, así que aquí no se filtra por + * project_id (es un campo informativo que puede ser null). + */ +export async function getTemplates(): Promise { const db = await getDb(); const rows = await db.getAllAsync