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 5 — desvio consciente vs. as outras apps, justificado pelo board ao vivo (optimistic update + refetch + reconciliação por WS)
Drag-and-drop@dnd-kit/core — sem @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 é vinculado a uma ou mais escolas, cada vínculo com o papel que vale só naquela escola. Com uma escola só, o cliente não envia X-School-Id e o backend resolve a escola principal do usuário. Com várias (ou para o Admin Master, que alcança todas), o operador escolhe a escola e o cliente envia X-School-Id; uma escola fora dos vínculos responde 403 com code: "school_forbidden", e o cliente descarta a escolha e volta ao seletor.
  • 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).

No tablet (toque), deslizar o dedo rola a coluna e o quadro; para mover um card é preciso segurar ~250 ms e então arrastar (vibra, onde houver suporte, quando o card descola). No mouse o arrasto começa depois de 6 px de movimento.

Confirmação em ações sensíveis: soltar um card em Pronto (libera a catraca e avisa o responsável) ou em Retirado (encerra a retirada, sem volta pelo monitor) abre um diálogo de confirmação antes de chamar a API; cancelar deixa o card onde estava. Estender +30 min, Revogar, Tirar do sinal e Sair também pedem confirmação. Só o passo Aguardando → Em preparação é direto, por ser interno à escola e o mais frequente.

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, acende o selo EM ATRASO e avisa o responsável
Revogar liberaçãoPOST /v1/operator/students/{id}/revoke-exitDesfaz o "Pronto": o aluno volta à presença real, on_premises ou off_premises para quem não registrou entrada (o card sai do quadro), a catraca é revogada, a sessão "Estou Chegando" do responsável é encerrada (revoked, quando nenhum irmão do mesmo sinal continua a caminho) e o responsável é notificado (pickup_reverted). Broadcast com reason: exit_revoked
Tirar do sinalPOST /v1/operator/students/{id}/remove-from-signal ({ expectedVersion })Tira um aluno do sinal (colunas Aguardando, Em preparação ou Pronto), veja a seção 5.4. Broadcast com reason: removed_from_signal
  • 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.

Selo EM ATRASO: aparece quando o operador estende a liberação, com o tempo correndo desde a primeira extensão (extensões seguintes não reiniciam o relógio). É marcador, não estado: o aluno continua na coluna Pronto e continua liberado na catraca — daí a legenda "o aluno segue liberado para sair" ao passar o mouse. Some quando a retirada é confirmada, quando a liberação é revogada e quando a reversão automática devolve o aluno à escola. Quantas extensões acendem o selo é configurável (padrão: a primeira).

Selo SEM REGISTRO DE ENTRADA: aparece quando o responsável avisou a chegada de um aluno que não passou pela entrada hoje (campo onPremises: false no cartão). Também é marcador, não estado: o aluno segue a coluna normal e pode ser separado e liberado. Serve para a portaria conferir se a criança está mesmo na escola. Some sozinho quando a entrada é registrada durante o sinal (leitor ou registro manual); o aluno continua na mesma coluna. Ver presença real.

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.

5.4 Tirar um aluno do sinal ​

Serve para quando o responsável avisou a chegada por um irmão que não vai sair. Cada aluno de um card em Aguardando, Em preparação ou Pronto tem o botão Tirar do sinal. No card-família o botão fica na linha de cada irmão, e o operador escolhe exatamente qual sai.

  • O aluno volta à presença real: na escola se tem registro de entrada (inclusive uma entrada feita durante o sinal), fora da escola se não tem.
  • O aluno deixa de fazer parte do sinal: some do quadro e da tela "Chegando" do app do responsável.
  • Em Pronto, a liberação da catraca também é revogada.
  • Se era o último aluno do sinal (os demais já foram retirados ou não havia outros), o sinal é encerrado com o motivo removed. O responsável que avisou recebe o push "Aviso de chegada encerrado" (evento arrival_signal_closed, desligável nas preferências de notificação). Enquanto sobra algum irmão no sinal, ninguém é notificado.
  • O diálogo de confirmação avisa quando a ação vai encerrar o sinal.
  • A Fila de saída do painel administrativo tem a mesma ação (POST /operator/queue/remove-from-signal), com a escolha do irmão dentro do diálogo.

Exige a mesma permissão de mover o card (arrival_signals.update_status). A versão desatualizada do card responde 409, e um aluno que não está em sinal ativo responde 422.

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 naquela escola — o papel do vínculo com a escola do canal, não o de outra escola do mesmo operador. É 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.
  • O selo EM ATRASO também chega por aqui: quem estendeu a liberação numa aba vê o selo acender nas outras, sem refresh.

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