Aplicação web para controle de ocupação de leitos hospitalares em tempo real, desenvolvida como trabalho da disciplina de Programação Web.
Este repositório contém a fundação estrutural do projeto: a arquitetura completa já definida e implementada de ponta a ponta, com um recorte reduzido do domínio. Todas as decisões técnicas que valerão para o sistema final já estão tomadas e materializadas em código — o que falta é extensão de funcionalidade, não redefinição de arquitetura.
Hospitais perdem visibilidade sobre a própria ocupação. Leitos ficam ociosos enquanto pacientes aguardam, porque a informação de qual leito está livre circula em fichas de papel, planilhas paralelas ou pelo rádio da equipe. A cada troca de turno, o retrato da ocupação se desatualiza; quem precisa decidir sobre capacidade decide com dados de horas atrás.
O SiGOH centraliza esse controle: cada mudança de status de leito é registrada uma única vez, com autoria e horário, e aparece imediatamente para todos que estão vendo o mapa.
Estrutura física. O hospital se organiza em três níveis: Bloco → Enfermaria → Leito.
Um leito pertence a exatamente uma enfermaria, que pertence a exatamente um bloco.
Ciclo de vida do leito. Um leito assume um de quatro estados:
| Status | Significado |
|---|---|
DISPONIVEL |
Livre para receber paciente |
OCUPADO |
Em uso |
LIMPEZA |
Higienização após alta, ainda indisponível |
BLOQUEADO |
Fora de operação (manutenção, interdição) |
O ciclo operacional normal é DISPONIVEL → OCUPADO → LIMPEZA → DISPONIVEL. BLOQUEADO é um
estado administrativo que sai desse ciclo e só retorna por ação explícita.
Auditoria. Toda mudança de status gera um registro imutável em historico_leitos
(status anterior, status novo, autor, horário). A tabela é append-only: nenhuma rotina da
aplicação a atualiza ou remove.
Taxa de ocupação. Calculada no servidor, sempre derivada dos leitos (nunca armazenada
no leito, para não haver risco de o agregado divergir da origem). Leitos BLOQUEADO são
excluídos do denominador — não estão disponíveis para uso, então contá-los distorceria o
indicador para baixo.
| Ator | Perfil | Pode fazer |
|---|---|---|
| Visitante | — (não autenticado) | Apenas a tela de login |
| Usuário Comum | ROLE_USER |
Consultar o mapa de leitos e as taxas de ocupação |
| Administrador | ROLE_ADMIN |
Tudo do usuário comum + administrar blocos, enfermarias e leitos, e alterar status |
Back-end e front-end são projetos independentes que se comunicam exclusivamente por HTTP/JSON. O acoplamento entre eles é apenas o contrato de dados, não a tecnologia — o mesmo back-end poderia atender um aplicativo móvel sem alteração alguma.
┌────────────────────┐ HTTP / JSON ┌──────────────────────┐ JDBC ┌──────────────┐
│ SPA React + TS │ ───────────────▶ │ API Spring Boot │ ─────────▶ │ PostgreSQL │
│ (navegador) │ ◀─────────────── │ (Spring Security) │ ◀───────── │ │
└────────────────────┘ └──────────────────────┘ └──────────────┘
▲ │
└──────── WebSocket / STOMP ───────────────┘
(push de mudanças de status)
Organização por funcionalidade (package by feature): cada domínio concentra suas próprias classes num único pacote, em vez de espalhá-las por pastas genéricas de controllers, services e repositories. Localizar tudo sobre "leito" é olhar uma pasta só.
com.sigoh.api
├── config/ Segurança (JWT, CORS), WebSocket, carga de dados de desenvolvimento
├── common/ Enums de domínio e tratamento centralizado de exceções
├── auth/ Autenticação (emissão de token)
├── usuario/ Usuário e integração com o Spring Security
├── bloco/ ─┐
├── enfermaria/ ├─ Estrutura física, com CRUD completo
├── leito/ ─┘
├── historico/ Trilha de auditoria (append-only)
├── ocupacao/ Cálculo de taxas e rotina de snapshot diário
└── realtime/ Publicação de eventos no canal WebSocket
Dentro de cada pacote, o fluxo é sempre o mesmo:
Controller → recebe a requisição, valida o DTO de entrada, devolve o DTO de saída
↓
Service → regra de negócio e fronteira transacional
↓
Repository → acesso a dados (Spring Data JPA)
↓
Entity → mapeamento objeto-relacional
A entidade JPA nunca é devolvida diretamente na resposta HTTP. Tudo passa por um Mapper
que a converte em DTO, para que mudanças internas do modelo não quebrem o contrato público
da API.
src/
├── pages/ Telas (Login, MapaLeitos, GestaoEstrutura)
├── services/ Comunicação com a API — um módulo por domínio
├── context/ Estado de autenticação compartilhado
├── hooks/ Assinatura do canal de tempo real
├── routes/ Proteção de rotas por perfil
├── types/ Contratos espelhando os DTOs do back-end
└── styles/ Folha de estilo única, com paleta verificada para contraste
Nenhum componente chama fetch diretamente: tudo passa por services/api.ts, que centraliza
URL base, cabeçalho de autenticação e tratamento de erro.
O modelo é inerentemente relacional: blocos contêm enfermarias, que contêm leitos, e o
histórico referencia leitos e usuários. Chaves estrangeiras e CHECK constraints garantem no
próprio banco invariantes que, de outra forma, dependeriam de disciplina do código da aplicação.
Além disso, as consultas mais importantes do sistema são agregações (quantos leitos em cada status, por bloco) — exatamente onde SQL é mais forte. Essas contagens são feitas pelo banco, não trazendo linhas para a memória da aplicação só para contá-las.
O docker-compose.yml sobe o banco pronto, então nenhum integrante precisa instalar e
configurar PostgreSQL na própria máquina.
O schema é tratado como código: cada alteração é um script SQL numerado e versionado junto
com a aplicação (db/migration/V1__schema_inicial.sql). O Flyway mantém uma tabela de
controle com o que já foi aplicado e o checksum de cada script — se alguém editar uma
migration já executada, a aplicação recusa subir em vez de aplicar silenciosamente algo
diferente do que está em produção.
A alternativa seria deixar o Hibernate gerar o schema sozinho (ddl-auto: update). Foi
descartada: não há histórico de mudanças, não há revisão possível em code review, e uma
alteração mal interpretada pode causar perda de dados. A configuração usada é
ddl-auto: validate — o Hibernate apenas confere se as entidades batem com as tabelas e
falha na inicialização se divergirem, o que expõe o erro cedo em vez de na primeira consulta.
O servidor não guarda sessão. O token é auto-contido e assinado (HMAC-SHA): a cada requisição, o servidor apenas confere a assinatura, sem consultar banco nem memória de sessão. Isso é o que permite, no futuro, rodar várias instâncias da API atrás de um balanceador sem precisar de sessão compartilhada.
Senhas são armazenadas com BCrypt, que embute salt por hash e tem custo computacional configurável — não se guarda senha em texto claro nem hash simples.
A regra é: leitura liberada a qualquer autenticado, escrita restrita a ADMIN. Ela vive em
SecurityConfig, no servidor.
O front-end também esconde botões que o usuário não pode usar, mas isso é conveniência de
interface, não mecanismo de segurança — código que roda no navegador do usuário pode ser
inspecionado e contornado. Uma requisição DELETE forjada por um usuário comum é recusada
com HTTP 403 mesmo que ele nunca tenha visto o botão.
O mapa de leitos é olhado por várias pessoas simultaneamente. Sem um canal de push, cada tela só descobriria uma mudança ao recarregar — ou por polling, que desperdiça requisições e ainda assim chega atrasado.
A API publica um evento em /topic/ocupacao a cada mudança de status; o front-end assina esse
tópico e atualiza a tela sem recarregar. O broker usado é o embutido do Spring, que mantém
as assinaturas em memória: suficiente para uma instância da aplicação. Escalar para várias
exigiria um broker externo (RabbitMQ, ActiveMQ) — decisão deliberadamente adiada até que essa
necessidade exista de fato.
Só existe um caminho para alterar o status de um leito: LeitoService.aplicarMudancaStatus().
Ele grava o leito, grava o registro de auditoria e publica o evento de tempo real, tudo dentro
da mesma transação — ou as três coisas acontecem, ou nenhuma acontece.
Essa concentração é intencional: se cada controller pudesse alterar status por conta própria, mais cedo ou mais tarde alguma alteração escaparia da auditoria.
Excluir um leito o marca como inativo em vez de removê-lo. O histórico de auditoria já registrado continua apontando para um registro existente — apagar a linha quebraria a trilha. Blocos e enfermarias, por não serem referenciados pelo histórico, têm exclusão física, mas protegida: o servidor recusa excluir um bloco que ainda tenha enfermarias, para não deixar leitos órfãos.
Uma rotina agendada (@Scheduled) grava, ao fim de cada dia, a ocupação consolidada de cada
bloco. Relatórios históricos consultam essa tabela em vez de reprocessar todo o histórico de
eventos a cada consulta. A rotina é idempotente — rodar duas vezes no mesmo dia não duplica
registros.
Requisitos de acessibilidade são muito mais caros de adaptar depois do que de nascer junto com a interface. O que já está aplicado:
- Marcação semântica:
<header>,<nav>,<main>,<footer>,<table>com<th scope>e<caption> - Todo campo de formulário tem
<label for>associado; rótulos em linha usam classe visualmente oculta mas legível por leitor de tela - Mensagens de erro com
role="alert", anunciadas assim que surgem - Indicador de foco visível (
:focus-visible) em todo elemento interativo - Paleta com razão de contraste verificada contra WCAG AA (≥ 4.5:1), anotada no CSS
- Status de leito comunicado por cor e texto — cor sozinha não é o único veículo de informação
O CSS é escrito à mão. Isso permitiu controlar e verificar o contraste de cada combinação de cores em vez de herdar uma paleta de terceiros, e mantém a superfície de dependências pequena.
Esta é a etapa de protótipo estrutural. A estratégia adotada foi construir a arquitetura inteira funcionando de ponta a ponta sobre um recorte pequeno do domínio, em vez de construir muitas telas sobre uma base ainda indefinida — assim qualquer funcionalidade nova é extensão de um padrão já validado, não invenção de um novo.
Já implementado:
- Modelo de dados completo (usuários, blocos, enfermarias, leitos, histórico, snapshots) em migration versionada
- Autenticação JWT com dois perfis e autorização aplicada no servidor
- CRUD completo de blocos e enfermarias; CRUD de leitos com atualização de status auditada
- Cálculo de taxa de ocupação por bloco
- Canal de tempo real ponta a ponta (servidor publica, front-end reage)
- Rotina agendada de snapshot diário
- Tratamento centralizado de erros com formato de resposta padronizado
- Front-end com login, mapa de leitos e administração de blocos
Próximos passos:
- Telas de administração de enfermarias e leitos (o padrão de
GestaoEstruturase repete) - Gráficos de ocupação (atual e histórico, sobre a tabela de snapshots)
- Tela de consulta ao histórico de um leito (o endpoint já existe)
- Visão mobile dedicada para atualização de status durante a ronda
- Persistência da sessão entre recarregamentos de página
Java 17+, Maven, Node.js 18+ e Docker (para o banco).
docker compose up -dSobe um PostgreSQL em localhost:5432 (base sigoh, usuário sigoh, senha sigoh).
O Flyway cria todas as tabelas automaticamente na primeira inicialização da API.
cd backend
mvn spring-boot:run -Dspring-boot.run.profiles=devDisponível em http://localhost:8080. O perfil dev carrega dados de demonstração
(dois usuários, dois blocos, três enfermarias e onze leitos):
| Senha | Perfil | |
|---|---|---|
admin@sigoh.com |
admin123 |
ROLE_ADMIN |
user@sigoh.com |
user123 |
ROLE_USER |
cd frontend
npm install
npm run devDisponível em http://localhost:5173, já apontando para a API local.
Para outro endereço, copie .env.example para .env e ajuste.
| Método | Rota | Perfil | Descrição |
|---|---|---|---|
POST |
/auth/login |
público | Autentica e devolve o token |
GET |
/api/blocos |
autenticado | Lista blocos |
POST PUT DELETE |
/api/blocos |
ADMIN |
Administra blocos |
GET |
/api/enfermarias?blocoId= |
autenticado | Lista enfermarias |
POST PUT DELETE |
/api/enfermarias |
ADMIN |
Administra enfermarias |
GET |
/api/leitos?blocoId=&enfermariaId= |
autenticado | Lista leitos |
GET |
/api/leitos/{id}/historico |
autenticado | Trilha de auditoria do leito |
POST PUT DELETE |
/api/leitos |
ADMIN |
Administra leitos |
PATCH |
/api/leitos/{id}/status |
ADMIN |
Altera o status (auditado) |
PATCH |
/api/leitos/{id}/avancar-status |
ADMIN |
Avança no ciclo operacional |
GET |
/api/ocupacao/blocos |
autenticado | Taxa de ocupação por bloco |
WS |
/ws → /topic/ocupacao |
— | Canal de eventos em tempo real |
| Integrante | Contribuição |
|---|---|
| (preencher) | (preencher) |