Skip to content

Monitor de Portaria (PWA)

O que é este documento — o mapa funcional e técnico do monitor de portaria do ionCLASS: o PWA que o funcionário de portaria/separação usa no horário de saída para chamar, preparar e liberar as crianças num kanban ao vivo. É uma das quatro superfícies do produto — veja as irmãs em App do Responsável, Painel Público e Painel Administrativo.


1. O que é

O monitor é a terceira superfície do ionCLASS: um PWA React para o operador de portaria (quem separa as crianças na saída). Como o app do responsável, é um cliente REST puro do backend — sem código compartilhado, só o contrato espelhado à mão (endpoints + schemas Zod). Três responsabilidades:

  1. Login do operador (Sanctum bearer).
  2. Configuração de trabalho — quais turmas/turnos/portões o operador cobre (é a porta de entrada; sem config o board fica bloqueado).
  3. Monitoramento — um board kanban com drag-and-drop, agrupado por sinal de chegada, atualizado ao vivo.

2. Stack técnica

CamadaTecnologia
RuntimeVite 6 + React 19 + TypeScript · package manager npm
UITailwind CSS 4 (@tailwindcss/vite), estilizado à mão com cn() (sem shadcn portado) · sonner (toasts) · lucide-react
Roteamentoreact-router 7 (SPA + guards)
Estado do board@tanstack/react-query 5desvio consciente vs. as outras apps, justificado pelo board ao vivo (optimistic update + refetch + reconciliação por WS)
Drag-and-drop@dnd-kit/coresem @dnd-kit/sortable: não há reordenação intra-coluna, só mover entre colunas
Tempo reallaravel-echo 2 + pusher-js 8 (Reverb, protocolo pusher)
Tipos / HTTPzod (fonte de verdade) · axios (espelha o cliente do app)
PWAvite-plugin-pwa (manifest + service worker; app-shell offline, API cross-origin nunca cacheada)

Comandos: npm run dev (porta 5174), npm run build (tsc --noEmit && vite build), npm run typecheck, npm run preview. Não há test runner — verificação é typecheck/build + smoke manual.

3. Login do operador

POST /api/v1/auth/operator-login{ token, user }, restrito ao operador. Uma conta válida de outro papel recebe "Apenas operadores podem acessar o monitor.". O token vive no localStorage do navegador; o interceptor de request seta Authorization: Bearer …; um 401 limpa token/usuário e volta ao login. Logout: POST /v1/operator/logout.

  • Escopo por escola: o operador pertence a exatamente uma escola — sem header X-School-Id; o backend resolve a escola do próprio token. Contraste com o app do responsável, que é N:N e envia X-School-Id.
  • Ver Autenticação e Autorização.

4. Work config (porta de entrada)

A tela de configuração de trabalho seleciona turmas + turnos (opcional) + portões. O board fica bloqueado até haver ≥1 turma E ≥1 portão — o estado configured é derivado no backend e qualquer um dos dois vazio mantém o board off, redirecionando para a tela de configuração.

EndpointUso
GET /v1/operator/school-classesTurmas disponíveis para o operador
GET /v1/operator/gatesPortões/pontos de retirada da escola
GET /v1/operator/work-configConfig atual ({ schoolClassIds, shifts, pickupPointIds, configured })
PUT /v1/operator/work-configSalva a config

5. Kanban ao vivo

O board puxa GET /v1/operator/monitor/board (lista flat de cards) e os organiza em 4 colunas por estado de retirada:

ColunaSignificado
Aguardandoresponsável sinalizou, aluno ainda não foi preparado
Preparandooperador está buscando/preparando o aluno
Prontoaluno no ponto de retirada, aguardando o responsável
Retiradoretirada confirmada

Sem configuração de trabalho → GET .../board responde 409 code: "no_work_config" e o cliente redireciona à tela de configuração.

5.1 Agrupamento por sinal (card-família)

Os cards são agrupados client-side pelo sinal de chegada: irmãos que vieram no mesmo "Estou Chegando" viram um card-família e se movem juntos entre colunas. O contexto de chegada vem em card.arrival (nullable): { signalId, origin, verifyIdentity, signaledAt, transportMode, by:{name,photoUrl}, vehicle, studentCount }.

5.2 Drag-and-drop e transições

As regras de movimento no cliente espelham o backend: só avanço sequencial — retrocesso/salto são bloqueados no drop (toast).

MutationEndpointEfeito
AvançarPOST /v1/operator/students/{id}/advance ({ toState, expectedVersion })Um passo à frente na sequência
Fechar retiradaPOST /v1/operator/students/{id}/confirm-pickup ({ expectedVersion })De Pronto para Retirado
Estender liberaçãoPOST /v1/operator/students/{id}/extend-exitLiberação física (coluna Pronto): +30 min
Revogar liberaçãoPOST /v1/operator/students/{id}/revoke-exitLiberação física (coluna Pronto): revoga a saída
  • Otimismo + reconciliação: o drag aplica a mudança no cache incrementando a versão do estado de retirada; em erro faz rollback; ao concluir invalida o board.
  • Trava otimista por versão: 409 = "estado mudou" (corrida — outro operador/hardware já mudou), 422 = salto ilegal.

5.3 Cartão e toggles

Cartão: nome, matrícula, foto, contexto de chegada (quem retira, transporte, veículo, flag verificar identidade, nº de irmãos) e badge do portão. As fotos vêm de GET /api/v1/students/{id}/photo (endpoint público, sem auth) direto num <img>.

Toggles: esconder fotos (LGPD), som em nova chegada, e refresh manual.

6. Tempo real

GET /v1/operator/monitor/credentials devolve os params do Reverb + o nome do canal privado school.{id}.monitor. O cliente assina via laravel-echo e escuta .StudentStateChanged (o ponto no prefixo vem do nome público do evento); o handshake do canal é Sanctum em /api/broadcasting/auth. A autorização do canal exige permissão de monitoramento ao vivo e poder ver a escola — a mesma regra do painel web e do operador PWA.

  • O payload do evento é fino e enxuto: id do aluno, estado de retirada e a versão do estado, entre outros campos mínimos.
  • A reconciliação nunca regride a versão (compara a versão do estado de retirada — estado nunca volta atrás).
  • Um aluno que ficou tempo demais em Pronto gera um toast de aviso.

Ver Tempo Real, Ciclo de Retirada e o runbook Runbook de Tempo Real.

7. Ver também

⚠️ Nota de contrato: o cliente espelha à mão a API versionada do backend (endpoints + schemas Zod). Ao mudar um shape no backend, os dois lados mudam em lockstep — nada garante isso automaticamente.

Documentação do ionCLASS