3f240e5277
Web: - IssueTask + IssueComment (modelos, migraciones, soft-deletes, campos de sync). Issue gana tasks()/comments() y accessor de % de avance derivado de tareas. - IssueDetail (página): checklist con asignado/fecha límite/progreso, hilo de comentarios con foto por comentario, galería de fotos de la incidencia y flujo de verificación open→in_review→resolved/closed (+reabrir) con notas. - Creación/edición en páginas propias (IssueForm), sin modal; al guardar redirige al detalle. Rutas projects.issues.create/edit/show. - Listado con tabla Rappasoft (IssueTable): filtros por estado/prioridad, búsqueda, barra de progreso y acciones por fila gateadas por permisos; IssueManager queda como contenedor (cabecera + stats) que embebe la tabla. - Seguridad: pertenencia al proyecto + permisos por acción (view/create/edit/delete issues, upload/delete media) en todos los componentes. API móvil (offline): - /sync: issue_task.create/update y issue_comment.create (idempotente, LWW). - /media: parent_entity issue_task / issue_comment. - bundle + tombstones incluyen issue_tasks / issue_comments. - openapi.yaml + MOBILE_SYNC_PROTOCOL.md actualizados. Tests: MobileApiTest 23 passing (+5); IssuesTablePageTest (3) smoke de la tabla. Branding: logo RTE International — MAI Group (public/images/logo-rte.png) en login y navegación; application-logo pasa de SVG por defecto a <img>. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
203 lines
6.9 KiB
YAML
203 lines
6.9 KiB
YAML
openapi: 3.0.3
|
|
info:
|
|
title: ConstruProgress Mobile API
|
|
version: "1.0.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 <token>`. See docs/MOBILE_SYNC_PROTOCOL.md.
|
|
servers:
|
|
- url: /api/v1
|
|
security:
|
|
- bearerAuth: []
|
|
paths:
|
|
/login:
|
|
post:
|
|
summary: Issue a device token
|
|
security: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [email, password, device_name]
|
|
properties:
|
|
email: { type: string, format: email }
|
|
password: { type: string }
|
|
device_name: { type: string }
|
|
app_version: { type: string, nullable: true }
|
|
responses:
|
|
"200":
|
|
description: Token issued
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
token: { type: string }
|
|
user: { $ref: '#/components/schemas/User' }
|
|
"422": { description: Invalid credentials }
|
|
/me:
|
|
get:
|
|
summary: Current user + effective permissions
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
user: { $ref: '#/components/schemas/User' }
|
|
"401": { description: Unauthenticated }
|
|
/logout:
|
|
post:
|
|
summary: Revoke the current device token
|
|
responses:
|
|
"200": { description: Logged out }
|
|
/projects:
|
|
get:
|
|
summary: Projects the user can access
|
|
responses:
|
|
"200": { description: OK }
|
|
/projects/{project}/bundle:
|
|
get:
|
|
summary: Offline bundle (full, or delta when `since` is given)
|
|
parameters:
|
|
- name: project
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- name: since
|
|
in: query
|
|
required: false
|
|
description: >
|
|
ISO8601 timestamp. Returns only records changed after it, plus
|
|
`deleted` tombstones. MUST be URL-encoded (the `+` offset).
|
|
schema: { type: string, format: date-time }
|
|
responses:
|
|
"200":
|
|
description: Bundle
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Bundle' }
|
|
"403": { description: Not a member of the project }
|
|
/templates:
|
|
get:
|
|
summary: Inspection templates for accessible projects (with version/hash)
|
|
parameters:
|
|
- name: since
|
|
in: query
|
|
required: false
|
|
schema: { type: string, format: date-time }
|
|
responses:
|
|
"200": { description: OK }
|
|
/sync:
|
|
post:
|
|
summary: Push a batch of offline mutations (idempotent by uuid)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [operations]
|
|
properties:
|
|
operations:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/Operation' }
|
|
responses:
|
|
"200":
|
|
description: Per-operation results
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
results:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/OperationResult' }
|
|
/media:
|
|
post:
|
|
summary: Upload a file (multipart) and attach it to a parent record
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required: [uuid, parent_entity, parent_id, file]
|
|
properties:
|
|
uuid: { type: string, format: uuid }
|
|
parent_entity: { type: string, enum: [feature, issue, issue_task, issue_comment, project, phase, layer] }
|
|
parent_id: { type: integer }
|
|
file: { type: string, format: binary }
|
|
category: { type: string, enum: [image, document, other] }
|
|
description: { type: string }
|
|
responses:
|
|
"200": { description: applied | duplicate }
|
|
"403": { description: Forbidden }
|
|
components:
|
|
securitySchemes:
|
|
bearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
schemas:
|
|
User:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
email: { type: string }
|
|
roles: { type: array, items: { type: string } }
|
|
permissions: { type: array, items: { type: string } }
|
|
Operation:
|
|
type: object
|
|
required: [entity, op, uuid, data]
|
|
properties:
|
|
entity: { type: string, enum: [progress_update, inspection, issue, issue_task, issue_comment, feature] }
|
|
op: { type: string, enum: [create, update] }
|
|
uuid: { type: string, format: uuid, description: client-generated idempotency key }
|
|
client_updated_at: { type: string, format: date-time }
|
|
data: { type: object }
|
|
example:
|
|
entity: feature
|
|
op: update
|
|
uuid: 0f8e...-uuid
|
|
client_updated_at: "2026-06-18T12:00:00+00:00"
|
|
data: { id: 5, status: completed, progress: 100 }
|
|
OperationResult:
|
|
type: object
|
|
properties:
|
|
uuid: { type: string, format: uuid }
|
|
status: { type: string, enum: [applied, duplicate, conflict, error] }
|
|
server_id: { type: integer, nullable: true }
|
|
error: { type: string, nullable: true }
|
|
server: { type: object, nullable: true, description: current server value on conflict }
|
|
Bundle:
|
|
type: object
|
|
properties:
|
|
server_time: { type: string, format: date-time }
|
|
project: { type: object }
|
|
phases: { type: array, items: { type: object } }
|
|
layers: { type: array, items: { type: object } }
|
|
features: { type: array, 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 } }
|
|
media: { type: array, items: { type: object } }
|
|
deleted:
|
|
type: object
|
|
description: tombstones (ids of soft-deleted records) when `since` is given
|
|
properties:
|
|
phases: { type: array, items: { type: integer } }
|
|
layers: { type: array, items: { type: integer } }
|
|
features: { type: array, items: { type: integer } }
|
|
inspections: { type: array, items: { type: integer } }
|
|
issues: { type: array, items: { type: integer } }
|
|
issue_tasks: { type: array, items: { type: integer } }
|
|
issue_comments: { type: array, items: { type: integer } }
|