Exemplo Prático: Jornada do Paciente — Solicitação de Atendimento Online
Este guia descreve, passo a passo, a jornada completa de um paciente solicitando um atendimento online. O fluxo cobre da autenticação à entrada na sala de atendimento, incluindo questionário clínico, upload de exames, gestão de fila com atualização em tempo real e a sessão com chat e vídeo.
Visão Geral do Fluxo
text
[Login] -> [Home do Paciente] -> [Solicitar Atendimento]
-> [Questionário + Upload de Exames]
-> [Fila de Espera (posição dinâmica)]
-> [Match com Médico]
-> [Sala de Atendimento: Chat + Video]Cada etapa é suportada por camadas específicas da arquitetura:
- Apps: Roteamento, RBAC e composição de domínios
- Domains: Consultation, Users, Chat, Medical-Record (quando aplicável)
- Core: WebSocket, API, Domain Events, VideoCall Adapter
- Shared: UI, Hooks, Utils
Passo 1: Autenticação (RBAC)
- Ação: o paciente realiza login.
- Camada:
Apps/AppPatient+Shared/Auth. - Mecanismo: Rotas do paciente protegidas com
RequireRoleLayout role="PATIENT"e lazy-loading das páginas.
tsx
// Roteamento do App do Paciente
<Route element={<RequireRoleLayout role="PATIENT" />}>
<Route path="/patient" element={<PatientHome />} />
<Route path="/patient/request" element={<RequestCareRoute />} />
<Route path="/patient/queue" element={<QueueRoute />} />
<Route path="/patient/room/:roomId" element={<PatientRoomRoute />} />
</Route>Passo 2: Página Principal (Home do Paciente)
- Ação: visualizar opções e iniciar “Solicitar Atendimento”.
- Camada:
Apps/AppPatient(UI orquestradora) +Domains/Consultation(quando a jornada iniciar). - Decisão: Apenas após o usuário acionar a jornada, os recursos do domínio são injetados.
tsx
// Ao entrar na rota de solicitação, injetamos o domínio necessário
const RequestCareRoute = () => {
useInjectReducer({ key: 'consultation', reducer: consultationReducer });
useInjectSaga({ key: 'consultation', saga: consultationSaga });
return <RequestCarePage />;
};Passo 3: Solicitar Atendimento (Questionário Clínico)
- Ação: o paciente preenche um questionário (sintomas, duração, alergias) e confirma a solicitação.
- Camada:
Domains/Consultation/UI + Store. - Mecanismo: Saga com guards valida campos obrigatórios e idempotência no envio.
typescript
// packages/domains/consultation/store/sagas.ts (trecho)
import { call, put, select, takeLatest } from 'redux-saga/effects';
import { actions as consultationActions } from './slice';
import { selectIsSubmittingRequest } from './selectors';
import { consultationApi } from '../api';
function* handleRequestOnlineCare(action: ReturnType<typeof consultationActions.requestCare>) {
// Guard de Reentrada (impede múltiplos envios)
const inFlight = yield select(selectIsSubmittingRequest);
if (inFlight) return;
// Guard de Dados Clínicos mínimos
const { symptoms, duration } = action.payload;
if (!symptoms || !duration) {
yield put(consultationActions.formError('Sintomas e duração são obrigatórios.'));
return;
}
try {
yield put(consultationActions.setSubmitting(true));
const ticket = yield call(consultationApi.requestCare, action.payload);
yield put(consultationActions.requestCareSuccess(ticket));
} catch (e: any) {
yield put(consultationActions.requestCareFailure(e.message));
} finally {
yield put(consultationActions.setSubmitting(false));
}
}
export function* consultationSaga() {
yield takeLatest(consultationActions.requestCare.type, handleRequestOnlineCare);
}Passo 4: Upload de Arquivos de Exames (Pré-Atendimento)
- Ação: anexar imagens/PDFs de exames.
- Camada:
Domains/Consultation(ouDomains/Medical-Record, dependendo da divisão de responsabilidade). - Mecanismo: Saga com validações de tamanho e tipo; upload com feedback de progresso.
typescript
// Guard de upload
function* handleUploadExam(action: ReturnType<typeof consultationActions.uploadExam>) {
const { file } = action.payload;
if (file.size > 20 * 1024 * 1024) {
yield put(consultationActions.formError('Arquivo acima de 20MB.')); return;
}
if (!/(pdf|png|jpg|jpeg)$/i.test(file.type)) {
yield put(consultationActions.formError('Formato inválido.')); return;
}
// Chamada para API de upload
yield call(consultationApi.uploadExam, file);
yield put(consultationActions.uploadExamSuccess());
}UI recomendada (Shared Design System):
- Componente
FileDropzonecom estados: vazio, arrastando, carregando e concluído. - Preview e opção de remover arquivo antes de enviar.
Passo 5: Entrada na Fila (XState — queueMachine)
- Ação: ao concluir o questionário e anexos, o paciente é colocado na fila virtual.
- Camada:
Domains/Consultation/Machines. - Mecanismo: Máquina de estados gerencia o status da fila e expõe posição atual via contexto.
typescript
// packages/domains/consultation/machines/queueMachine.ts
import { createMachine, assign } from 'xstate';
type Ctx = { ticketId: string; position: number; estimatedWaitMin: number };
export const queueMachine = createMachine<Ctx>({
id: 'queue',
initial: 'emFila',
context: { ticketId: '', position: 0, estimatedWaitMin: 0 },
states: {
emFila: {
entry: 'subscribeQueueUpdates',
on: {
QUEUE_UPDATED: {
actions: assign({ position: (_ctx, e: any) => e.position, estimatedWaitMin: (_ctx, e: any) => e.eta })
},
MATCH_FOUND: { target: 'pareado' }
},
exit: 'unsubscribeQueueUpdates'
},
pareado: { type: 'final' }
}
});Atualização de posição em tempo real:
- Assinatura via
Core/WebSocketou viaDomain Eventsretransmitidos ao front-end. - A UI do paciente exibe “Você é o 3º na fila (≈ 6 min)”, atualizando a cada evento
QUEUE_UPDATED.
Passo 6: Match e Redirecionamento Automático
- Ação: quando um médico fica disponível, o backend emite
CONSULTATION_MATCHED. - Camada:
Core/DomainEvents+Domains/Consultation/Store. - Mecanismo: Saga escuta eventos do barramento e redireciona para a sala virtual.
typescript
// Redirecionamento ao receber MATCH
import { take, put } from 'redux-saga/effects';
import { domainEventsBus } from 'core/domain-events';
import { DomainEventNames } from 'contracts/events';
import { push } from 'core/router';
export function* watchMatchEvent() {
const channel = domainEventsBus.createChannel();
while (true) {
const event = yield take(channel);
if (event.name === DomainEventNames.CONSULTATION_MATCHED) {
yield put(push(`/patient/room/${event.payload.roomId}`));
}
}
}Passo 7: Sala de Atendimento (Estados Paralelos: Vídeo + Chat)
- Ação: o paciente entra na sala e interage com o médico.
- Camada:
Domains/Consultation/Machines (sessionMachine)+Domains/Chat+Core/VideoCall. - Mecanismo: Máquina paralela orquestra vídeo e chat simultaneamente; guards asseguram consentimento antes de ligar a câmera.
typescript
// sessionMachine (trecho) com guard de consentimento
const sessionMachine = createMachine({
id: 'session',
type: 'parallel',
context: { isTermOfUseAccepted: false },
states: {
video: {
initial: 'aguardandoTermo',
states: {
aguardandoTermo: {
on: { ACCEPT_TERMS: { target: 'conectando', actions: assign({ isTermOfUseAccepted: (_c) => true }) } }
},
conectando: {
on: { START_VIDEO: { target: 'emChamada', cond: (ctx) => ctx.isTermOfUseAccepted } }
},
emChamada: { /* ... */ }
}
},
chat: { /* sincronização e envio de mensagens */ }
}
});UI recomendada para a Sala:
- Layout responsivo com vídeo principal e painel lateral de chat.
- Indicadores de estado (conectando, reconectando, mutado).
- Botão de “Aceitar Termos” bloqueando
START_VIDEOaté aceite (viastate.can).
Diagrama ASCII do Fluxo Completo
text
LOGIN -> HOME(PATIENT) --clicar--> SOLICITAR ATENDIMENTO
-> QUESTIONÁRIO + UPLOAD
-> [Saga valida + envia]
-> ENTRA NA FILA (queueMachine)
-> [QUEUE_UPDATED]* (posição: n..3..2..1)
-> MATCH_FOUND
-> REDIRECT /patient/room/:roomId
-> SESSÃO (XState paralela)
├─ Vídeo (guard: termo aceito) -> conectando -> em chamada
└─ Chat (ws) -> sincronizando -> prontoIntegração com a Arquitetura
- Injeção Dinâmica: apenas ao iniciar a jornada do atendimento, o estado e sagas de
consultationsão carregados. - Event-Driven: o match é comunicado por
CONSULTATION_MATCHED; o domínio deChatreage independentemente para iniciar a sala. - RBAC: rotas do paciente são protegidas, evitando acesso não autorizado a outras jornadas.
- Observabilidade: transições das máquinas podem ser logadas (ver página "Lifecycle e Observabilidade").
Considerações de Segurança e Conformidade
- Consentimento: a câmera só ativa após aceite explícito do termo (guard na state machine).
- Dados Sensíveis: uploads limitados por tipo e tamanho; metadados clínicos tratados via
contractspara tipagem consistente. - Resiliência: fila e sessão toleram reconexões; chat permanece funcional mesmo durante instabilidades de vídeo.