Laudos e Presets

Endpoints para gerenciar laudos médicos dos estudos e presets de texto pré-preenchido. Ciclo de vida do laudo: pending → draft → final. Uma vez finalizado, o conteúdo só pode mudar via correção, que cria uma nova ReportRevision imutável no histórico. RBAC (importante): apenas usuários “doutores” (não tenant_admin, não super_admin) podem criar, editar ou corrigir laudos. Tentativas por admins retornam 403. Audit trail: toda ação em um laudo — criação de rascunho, atualização, finalização, correção — gera uma ReportRevision imutável, retornada por GET /studies/{id}/reports/revisions.

GET /api/studies//reports

Retorna o laudo do estudo (ou null se não existir). Auth: auth:api

Path params

ParamTipoDescrição
iduuidID do estudo

Response 200

{
  "study_id": "uuid",
  "report_status": "pending | draft | final | null",
  "report": {
    "id": "uuid",
    "study_id": "uuid",
    "template_id": "uuid | null",
    "preset_id": "uuid | null",
    "tenant_id": "uuid",
    "unit_id": "uuid",
    "status": "draft | final",
    "content": "string (HTML do Tiptap)",
    "finalized_at": "iso8601 | null",
    "created_at": "iso8601",
    "updated_at": "iso8601",
    "author": {
      "id": "uuid",
      "name": "string",
      "email": "string",
      "crm": "string | null",
      "signature": "base64 (JPEG/PNG)"
    },
    "template": { "id": "uuid", "name": "string" },
    "preset": { "id": "uuid", "name": "string" }
  }
}
Se não houver laudo, report vem null e report_status também. O campo signature do autor é normalmente hidden no model User, mas é exposto via makeVisible() aqui porque é usado na geração do PDF.

Erros

StatusSituação
401Token inválido
404Estudo não encontrado ou fora do tenant/unit

POST /api/studies//reports

Cria ou atualiza o laudo do estudo (upsert). Usado tanto para salvar rascunho quanto para finalizar. Auth: auth:apiapenas doutores (não tenant_admin, não super_admin)

Request body

content
string
required
Conteúdo do laudo em HTML (saída do editor Tiptap).
status
string
required
draft para salvar rascunho, final para finalizar.
template_id
uuid
ID do template usado. Deve pertencer ao tenant/unidade do estudo.
preset_id
uuid
ID do preset aplicado. Deve pertencer ao tenant do estudo.

Response 200

{
  "message": "Laudo salvo com sucesso.",
  "study_id": "uuid",
  "report_status": "draft | final",
  "report": { "...": "mesmo shape do GET" }
}

Erros

StatusSituaçãoBody
403Usuário é tenant_admin ou super_admin{"message": "Apenas doutores podem criar e editar laudos."}
404Estudo não pertence ao tenant/unit
422Laudo já está final — use correção{"message": "Laudo já finalizado. Correções devem usar fluxo específico."}
422template_id/preset_id inválidos{"message": "Template inválido para o tenant/unidade do estudo."}
422Validação de campos{"message": "...", "errors": {...}}

Efeitos colaterais

  • Cria ReportRevision com action = draft_created (primeiro), draft_updated ou finalized
  • Atualiza study.report_status
  • Se status=final: atualiza study.reported_at e study.reported_by_user_id

POST /api/studies//reports/corrections

Registra uma correção num laudo já finalizado. O conteúdo anterior é preservado no histórico de revisões. Auth: auth:apiapenas doutores

Request body

content
string
required
Novo conteúdo do laudo em HTML.
reason
string
Motivo da correção (opcional, até 2000 caracteres).

Response 200

{
  "message": "Correção de laudo final registrada com sucesso.",
  "study_id": "uuid",
  "report_status": "final",
  "report": { "...": "mesmo shape do GET" }
}

Erros

StatusSituaçãoBody
403Usuário é admin{"message": "Apenas doutores podem criar e editar laudos."}
404Estudo ou laudo não existe
422Laudo não está final{"message": "Correção só é permitida para laudo finalizado."}

Efeitos colaterais

  • Cria ReportRevision com action = "correction", preservando previous_content, new_content e reason
  • Atualiza report.content, report.author_user_id, report.finalized_at = now()
  • Atualiza study.reported_at e study.reported_by_user_id

GET /api/studies//reports/revisions

Retorna o histórico completo de revisões do laudo (audit trail imutável). Auth: auth:api

Response 200

Array ordenado por created_at DESC (mais recente primeiro):
[
  {
    "id": "uuid",
    "report_id": "uuid",
    "author_user_id": "uuid",
    "action": "draft_created | draft_updated | finalized | correction",
    "previous_content": "string | null",
    "new_content": "string",
    "reason": "string | null",
    "created_at": "iso8601",
    "updated_at": "iso8601",
    "author": {
      "id": "uuid",
      "name": "string",
      "email": "string",
      "crm": "string | null",
      "signature": "base64"
    }
  }
]
Se o estudo não tiver laudo, retorna [] (array vazio, não 404).

Erros

StatusSituação
404Estudo não existe ou fora do tenant/unit

Presets de Laudo

Textos pré-preenchidos que o médico pode aplicar ao laudo com um clique. Escopados por tenant, opcionalmente por unidade e/ou modalidade.

GET /api/tenant/report-presets

Lista paginada de presets do tenant. Auth: auth:api (qualquer usuário autenticado)

Query params

unit_id
uuid
Filtra por unidade (presets com unit_id = null são “globais” do tenant).
modality
string
Filtra por modalidade (CT, CR, MG, US, etc.). Normalizado para maiúsculas.
Busca ILIKE em name.
is_active
boolean
Filtra por ativo/inativo. Aceita true/false/1/0 (o backend usa $request->boolean()).

Response 200

Paginação de 50 itens por página:
{
  "data": [
    {
      "id": "uuid",
      "tenant_id": "uuid",
      "unit_id": "uuid | null",
      "name": "string",
      "modality": "string | null",
      "study_description_contains": "string | null",
      "default_content": "string | null",
      "is_active": true,
      "sort_order": 0,
      "unit_name": "string | null",
      "created_at": "iso8601",
      "updated_at": "iso8601"
    }
  ],
  "current_page": 1,
  "per_page": 50,
  "total": 24
}

GET /api/tenant/report-presets/

Detalhe de um preset. Auth: auth:api

Response 200

{
  "id": "uuid",
  "tenant_id": "uuid",
  "unit_id": "uuid | null",
  "name": "string",
  "modality": "string | null",
  "study_description_contains": "string | null",
  "default_content": "string | null",
  "is_active": true,
  "sort_order": 0,
  "unit": { "id": "uuid", "name": "string" },
  "created_at": "iso8601",
  "updated_at": "iso8601"
}

Erros

StatusSituação
404Preset não encontrado ou fora do tenant

POST /api/tenant/report-presets

Cria um novo preset. Auth: auth:api + tenant_admin

Request body

name
string
required
Nome do preset (máximo 255 caracteres).
unit_id
uuid
Unidade à qual o preset pertence. Deve ser do mesmo tenant. null = preset global do tenant.
modality
string
Modalidade associada (máximo 16 caracteres, salvo em maiúsculas).
study_description_contains
string
Substring usada para sugestão automática no editor (match em study.study_description).
default_content
string
Conteúdo HTML pré-preenchido que é aplicado ao laudo quando o preset é selecionado.
is_active
boolean
default:"true"
Se false, o preset não aparece na seleção do editor.
sort_order
integer
default:"0"
Ordem de exibição na UI.

Response 201

Mesmo shape de GET /api/tenant/report-presets/{id}.

Erros

StatusSituação
403Usuário não é tenant_admin
422unit_id não pertence ao tenant, ou validação de campos

PATCH /api/tenant/report-presets/

Atualiza um preset existente. Todos os campos são opcionais. Auth: auth:api + tenant_admin

Request body

Mesmos campos de POST, todos opcionais (validação sometimes).

Response 200

Preset atualizado (mesmo shape de GET).

Erros

StatusSituação
403Usuário não é tenant_admin
404Preset não existe ou fora do tenant
422Validação

DELETE /api/tenant/report-presets/

Remove um preset. Auth: auth:api + tenant_admin

Response 200

{ "message": "Preset removido com sucesso." }

Erros

StatusSituação
403Usuário não é tenant_admin
404Preset não existe ou fora do tenant