Admin cria uma mensagem → Sistema gera automaticamente um “followup” (link de resposta)
Usuário recebe notificação na aplicação
Usuário clica na notificação/mensagem → Abre a mensagem completa dentro da aplicação
Dentro da mensagem, o frontend busca e exibe os botões de possíveis respostas
** Ideia de Interface**: Botões padrão “Sim”, “Não”, “Talvez” + botão “+” para mais opções
Combinação: Usuário pode escolher até 3 botões que concatenam palavras (ex: “Sim Confirmo”)
Resposta rápida: Clica em “Sim”, “Não” ou “Talvez”
Resposta combinada: Seleciona até 3 palavras (ex: “Sim Confirmo Presença”)
Quando uma mensagem é criada, o sistema automaticamente gera um ID único para cada destinatário
Este ID fica associado à mensagem e é enviado junto com ela
O número 36 é o ID do followup específico para aquele usuário e aquela mensagem
O frontend usa este ID para carregar os botões de resposta da tabela tiporesposta
Não há mais links HTML no conteúdo da mensagem
Este exemplo demonstra o fluxo completo desde a criação de uma mensagem até a resposta do usuário, focando nos endpoints de resposta por índice e resposta por tipo.
POST /api/mensagens
Authorization: Bearer {token}
Content-Type: application/json
{
"tipo": 2,
"tipoMensagemId": 2,
"titulo": "Confirmação de Presença - Evento Esportivo",
"descricaoBreve": "Confirme sua presença no evento esportivo",
"conteudo": "<h2>Evento Esportivo</h2><p>Dia: 15/01/2025</p><p>Horário: 14h00</p><p>Local: Ginásio Principal</p>",
"requerConfirmacao": true,
"dataLimiteResposta": "2025-01-14T23:59:59",
"entidadeId": 1,
"meiosEnvio": ["PushNotification", "Inbox", "Email"],
"destinatarios": [
{
"destinatarioId": "usuario-123",
"tipoDestinatario": "usuario",
"nivelHierarquico": 5,
"nomeDestinatario": "João Silva",
"emailDestinatario": "joao.silva@email.com"
}
]
}
{
"success": true,
"message": "Mensagem com follow-up criado e enviado com sucesso.",
"data": {
"id": 55,
"tipo": "Comunicado",
"tipoMensagemId": 2,
"titulo": "Confirmação de Presença - Evento Esportivo",
"conteudo": "<h2>Evento Esportivo</h2><p>Dia: 15/01/2025</p><p>Horário: 14h00</p><p>Local: Ginásio Principal</p>",
"status": "enviado",
"dataEnvio": "2025-01-08T10:30:00Z",
"followUpId": 36
}
}
// 1. Backend recebe a mensagem criada
var mensagemCriada = resultCriacao.Data; // Mensagem ID = 55
// 2. Para cada destinatário, cria um FollowUp individual
var followUps = new List<FollowUp>();
foreach (var dest in request.Destinatarios)
{
var followUp = new FollowUp
{
// ⭐ ID é gerado automaticamente pelo banco (IDENTITY)
Id = 0, // Será preenchido pelo banco
MensagemId = mensagemCriada.Id, // 55
UsuarioId = dest.DestinatarioId, // "usuario-123"
Status = StatusFollowUp.Aguardando,
DataEnvio = DateTime.Now,
TipoMensagemId = request.TipoMensagemId, // 2
TipoRespostaId = request.TipoRespostaId // null inicialmente
};
followUps.Add(followUp);
}
// 3. Insere todos os FollowUps no banco
_dbFollowUp.InserirVarios(followUps);
// 4. Após inserção, os IDs são preenchidos automaticamente
// followUp.Id agora tem o valor real (ex: 36)
// Após inserir no banco, o sistema obtém os IDs gerados
foreach (var followUp in followUps)
{
// O ID agora está disponível: followUp.Id = 36
// 5. O ID é associado à mensagem para uso posterior
// Não precisamos mais gerar links HTML no conteúdo
// O frontend usará este ID para carregar os botões de resposta
}
Mensagem ID: 55
FollowUp ID: 36 (gerado automaticamente)
Usuário específico: “usuario-123”
Conteúdo limpo: Sem links HTML adicionados
// 1. Quando a mensagem é carregada, o followUpId vem junto
const mensagem = {
id: 55,
titulo: "Confirmação de Presença - Evento Esportivo",
conteudo: "<h2>Evento Esportivo</h2><p>Dia: 15/01/2025</p>...",
followUpId: 36 // ⭐ ID do followup para este usuário
};
// 2. Frontend usa este ID para carregar botões de resposta
const followUpId = mensagem.followUpId; // 36
// 3. Carregar opções de resposta usando este ID
carregarOpcoesResposta(followUpId);
<!-- Conteúdo da notificação na aplicação -->
<div class="notification-card" onclick="abrirMensagem(55, 36)">
<h3>📢 Novo Comunicado</h3>
<h4>Confirmação de Presença - Evento Esportivo</h4>
<p><strong>Dia:</strong> 15/01/2025</p>
<p><strong>Horário:</strong> 14h00</p>
<p><strong>Local:</strong> Ginásio Principal</p>
<div class="status">
<span class="badge">Aguardando Resposta</span>
</div>
</div>
// Função chamada quando usuário clica na notificação
function abrirMensagem(mensagemId, followUpId) {
// 1. Carregar conteúdo completo da mensagem
carregarMensagemCompleta(mensagemId);
// 2. Carregar opções de resposta para este followup
carregarOpcoesResposta(followUpId);
// 3. Exibir modal ou expandir seção da mensagem
mostrarMensagemComRespostas();
}
// 1. ID vem da notificação/mensagem recebida
const notificacao = {
mensagemId: 55,
followUpId: 36,
titulo: "Confirmação de Presença - Evento Esportivo",
status: "Aguardando"
};
// 2. Usar o ID diretamente da notificação
const followUpId = notificacao.followUpId; // 36
// 3. Carregar opções de resposta usando este ID
async function carregarOpcoesResposta(followUpId) {
const response = await fetch(`/api/follow-up/${followUpId}/respostas-possiveis`);
const data = await response.json();
// Renderizar botões de resposta na mensagem
renderizarBotoesResposta(data.respostas, followUpId);
}
GET /api/follow-up/36/respostas-possiveis
Content-Type: application/json
{
"respostas": [
{
"id": 1,
"tipoResposta": "Sim",
"criacao": "2025-01-01T00:00:00Z",
"atualizacao": "2025-01-01T00:00:00Z"
},
{
"id": 2,
"tipoResposta": "Não",
"criacao": "2025-01-01T00:00:00Z",
"atualizacao": "2025-01-01T00:00:00Z"
},
{
"id": 3,
"tipoResposta": "Talvez",
"criacao": "2025-01-01T00:00:00Z",
"atualizacao": "2025-01-01T00:00:00Z"
},
{
"id": 4,
"tipoResposta": "Confirmo",
"criacao": "2025-01-01T00:00:00Z",
"atualizacao": "2025-01-01T00:00:00Z"
},
{
"id": 5,
"tipoResposta": "Presença",
"criacao": "2025-01-01T00:00:00Z",
"atualizacao": "2025-01-01T00:00:00Z"
},
{
"id": 6,
"tipoResposta": "Ausência",
"criacao": "2025-01-01T00:00:00Z",
"atualizacao": "2025-01-01T00:00:00Z"
}
]
}
<!-- Botões padrão sempre visíveis -->
<div class="botoes-padrao">
<button class="btn-sim" onclick="selecionarResposta(1)">✅ Sim</button>
<button class="btn-nao" onclick="selecionarResposta(2)">❌ Não</button>
<button class="btn-talvez" onclick="selecionarResposta(3)">❓ Talvez</button>
<button class="btn-mais" onclick="abrirOpcoesExtras()">➕</button>
</div>
<!-- Opções extras (aparecem quando clica no +) -->
<div class="opcoes-extras" id="opcoes-extras" style="display: none;">
<button class="btn-opcao" onclick="selecionarResposta(4)">Confirmo</button>
<button class="btn-opcao" onclick="selecionarResposta(5)">Presença</button>
<button class="btn-opcao" onclick="selecionarResposta(6)">Ausência</button>
<button class="btn-opcao" onclick="selecionarResposta(7)">Interessado</button>
</div>
<!-- Resposta selecionada -->
<div class="resposta-selecionada">
<span id="resposta-texto">Selecionadas: </span>
<button onclick="enviarResposta()" class="btn-enviar">Enviar</button>
</div>
let respostasSelecionadas = [];
const maxSelecoes = 3;
function selecionarResposta(tipoRespostaId) {
const resposta = obterRespostaPorId(tipoRespostaId);
if (respostasSelecionadas.includes(tipoRespostaId)) {
// Desmarcar se já estava selecionado
respostasSelecionadas = respostasSelecionadas.filter(id => id !== tipoRespostaId);
} else if (respostasSelecionadas.length < maxSelecoes) {
// Adicionar se ainda não atingiu o limite
respostasSelecionadas.push(tipoRespostaId);
}
atualizarInterface();
}
function atualizarInterface() {
const textoResposta = document.getElementById('resposta-texto');
const nomesRespostas = respostasSelecionadas.map(id => obterRespostaPorId(id).tipoResposta);
textoResposta.textContent = `Selecionadas: ${nomesRespostas.join(' ')}`;
// Destacar botões selecionados
document.querySelectorAll('.btn-sim, .btn-nao, .btn-talvez, .btn-opcao').forEach(btn => {
btn.classList.remove('selecionado');
});
respostasSelecionadas.forEach(id => {
const botao = document.querySelector(`[onclick="selecionarResposta(${id})"]`);
if (botao) botao.classList.add('selecionado');
});
}
function abrirOpcoesExtras() {
const opcoes = document.getElementById('opcoes-extras');
opcoes.style.display = opcoes.style.display === 'none' ? 'block' : 'none';
}
async function enviarResposta() {
if (respostasSelecionadas.length === 0) {
alert('Selecione pelo menos uma opção');
return;
}
try {
const response = await fetch(`/api/follow-up/${followUpId}/resposta-tipo`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tipoRespostaIds: respostasSelecionadas })
});
if (response.ok) {
alert('Resposta enviada com sucesso!');
// Atualizar interface para mostrar como respondido
}
} catch (error) {
console.error('Erro ao enviar resposta:', error);
}
}
Resposta simples: Clica em “Sim” → Resposta: “Sim”
Resposta combinada: Clica em “Sim” + “Confirmo” → Resposta: “Sim Confirmo”
Resposta complexa: Clica em “Sim” + “Confirmo” + “Presença” → Resposta: “Sim Confirmo Presença”
<!-- Modal ou seção expandida da mensagem -->
<div class="mensagem-completa" id="mensagem-55">
<div class="mensagem-header">
<h2>📢 Confirmação de Presença - Evento Esportivo</h2>
<span class="status-badge">Aguardando Resposta</span>
</div>
<div class="mensagem-conteudo">
<p><strong>Dia:</strong> 15/01/2025</p>
<p><strong>Horário:</strong> 14h00</p>
<p><strong>Local:</strong> Ginásio Principal</p>
<p><strong>Descrição:</strong> Evento esportivo anual da instituição</p>
</div>
<div class="mensagem-respostas" id="botoes-resposta">
<!-- 💡 Interface Proposta: Botões Padrão + Opções Extras -->
<!-- Botões padrão sempre visíveis -->
<div class="botoes-padrao">
<h4>Responder:</h4>
<button class="btn-sim" onclick="selecionarResposta(1)">✅ Sim</button>
<button class="btn-nao" onclick="selecionarResposta(2)">❌ Não</button>
<button class="btn-talvez" onclick="selecionarResposta(3)">❓ Talvez</button>
<button class="btn-mais" onclick="abrirOpcoesExtras()">➕</button>
</div>
<!-- Opções extras (aparecem quando clica no +) -->
<div class="opcoes-extras" id="opcoes-extras" style="display: none;">
<button class="btn-opcao" onclick="selecionarResposta(4)">Confirmo</button>
<button class="btn-opcao" onclick="selecionarResposta(5)">Presença</button>
<button class="btn-opcao" onclick="selecionarResposta(6)">Ausência</button>
<button class="btn-opcao" onclick="selecionarResposta(7)">Interessado</button>
</div>
<!-- Resposta selecionada -->
<div class="resposta-selecionada">
<span id="resposta-texto">Selecionadas: </span>
<button onclick="enviarResposta()" class="btn-enviar">Enviar</button>
</div>
</div>
</div>
<script>
// ⭐ ID vem da notificação/mensagem
const followUpId = 36; // Obtido da notificação
let respostasSelecionadas = [];
const maxSelecoes = 3;
// Função para selecionar/deselecionar resposta
function selecionarResposta(tipoRespostaId) {
if (respostasSelecionadas.includes(tipoRespostaId)) {
// Desmarcar se já estava selecionado
respostasSelecionadas = respostasSelecionadas.filter(id => id !== tipoRespostaId);
} else if (respostasSelecionadas.length < maxSelecoes) {
// Adicionar se ainda não atingiu o limite
respostasSelecionadas.push(tipoRespostaId);
}
atualizarInterface();
}
// Função para atualizar a interface
function atualizarInterface() {
const textoResposta = document.getElementById('resposta-texto');
const nomesRespostas = respostasSelecionadas.map(id => obterRespostaPorId(id).tipoResposta);
textoResposta.textContent = `Selecionadas: ${nomesRespostas.join(' ')}`;
// Destacar botões selecionados
document.querySelectorAll('.btn-sim, .btn-nao, .btn-talvez, .btn-opcao').forEach(btn => {
btn.classList.remove('selecionado');
});
respostasSelecionadas.forEach(id => {
const botao = document.querySelector(`[onclick="selecionarResposta(${id})"]`);
if (botao) botao.classList.add('selecionado');
});
}
// Função para abrir/fechar opções extras
function abrirOpcoesExtras() {
const opcoes = document.getElementById('opcoes-extras');
opcoes.style.display = opcoes.style.display === 'none' ? 'block' : 'none';
}
// Função para enviar resposta
async function enviarResposta() {
if (respostasSelecionadas.length === 0) {
alert('Selecione pelo menos uma opção');
return;
}
try {
const response = await fetch(`/api/follow-up/${followUpId}/resposta-tipo`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tipoRespostaIds: respostasSelecionadas })
});
if (response.ok) {
alert('Resposta enviada com sucesso!');
// Atualizar interface para mostrar como respondido
atualizarStatusResposta(respostasSelecionadas.map(id => obterRespostaPorId(id).tipoResposta).join(' '));
}
} catch (error) {
console.error('Erro ao enviar resposta:', error);
}
}
</script>
async function responderPorIndice(indice, followUpId) {
try {
const response = await fetch(`/api/follow-up/${followUpId}/resposta-indice`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ indiceResposta: indice })
});
const result = await response.json();
if (response.ok) {
// Atualizar interface para mostrar resposta enviada
atualizarStatusResposta('Sim', 'respondido');
alert(`Resposta 'Sim' enviada com sucesso!`);
} else {
alert(`Erro: ${result.message}`);
}
} catch (error) {
console.error('Erro ao enviar resposta:', error);
alert('Erro ao enviar resposta');
}
}
// Função para atualizar a interface após resposta
function atualizarStatusResposta(resposta, status) {
const statusBadge = document.querySelector('.status-badge');
statusBadge.textContent = `Respondido: ${resposta}`;
statusBadge.className = 'status-badge responded';
// Desabilitar botões de resposta
const botoesResposta = document.querySelectorAll('.btn-resposta-rapida');
botoesResposta.forEach(botao => botao.disabled = true);
}
POST /api/follow-up/36/resposta-indice
Content-Type: application/json
{
"indiceResposta": 0
}
{
"message": "Resposta 'Sim' registrada com sucesso."
}
// Backend processa automaticamente:
followUp.Resposta = "Sim"; // Palavra correspondente ao índice 0
followUp.Status = StatusFollowUp.True; // Marca como respondido
followUp.DataRetorno = DateTime.Now; // Registra data da resposta
followUp.TipoRespostaId = 1; // ID da palavra "Sim"
async function responderPorTipo() {
if (selecoes.length === 0) {
alert('Selecione pelo menos uma opção');
return;
}
if (selecoes.length > 3) {
alert('Máximo de 3 opções podem ser selecionadas');
return;
}
try {
const response = await fetch(`/api/follow-up/${followUpId}/resposta-tipo`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tipoRespostaIds: selecoes })
});
const result = await response.json();
if (response.ok) {
alert(`Resposta '${result.message}' enviada com sucesso!`);
// Redirecionar ou fechar página
} else {
alert(`Erro: ${result.message}`);
}
} catch (error) {
console.error('Erro ao enviar resposta:', error);
alert('Erro ao enviar resposta');
}
}
POST /api/follow-up/36/resposta-tipo
Content-Type: application/json
{
"tipoRespostaIds": [1, 4, 5]
}
{
"message": "Resposta 'Sim Confirmo Presença' registrada com sucesso."
}
// Backend processa automaticamente:
followUp.Resposta = "Sim Confirmo Presença"; // Concatenação das 3 palavras
followUp.Status = StatusFollowUp.True; // Marca como respondido
followUp.DataRetorno = DateTime.Now; // Registra data da resposta
followUp.TipoRespostaId = null; // Não categoriza automaticamente
GET /api/follow-up/mensagem/55
Content-Type: application/json
[
{
"id": 36,
"mensagemId": 55,
"usuarioId": "usuario-123",
"nomeDestinatario": "João Silva",
"emailDestinatario": "joao.silva@email.com",
"status": "True",
"resposta": "Sim Confirmo Presença",
"dataEnvio": "2025-01-08T10:30:00Z",
"dataRetorno": "2025-01-08T14:25:00Z"
}
]
| Fase | Ação | Endpoint | Resultado |
|------|------|----------|-----------|
| 1 | Criar mensagem | POST /api/mensagens | Mensagem criada + FollowUp gerado |
| 2 | Usuário recebe notificação | - | Notificação na aplicação com ID |
| 3 | Usuário clica na mensagem | - | Abre mensagem completa |
| 4 | Carregar opções | GET /api/follow-up/{id}/respostas-possiveis | Botões da tabela tiporesposta |
| 5 | Resposta por índice | POST /api/follow-up/{id}/resposta-indice | Resposta única registrada |
| 5 | Resposta por tipo | POST /api/follow-up/{id}/resposta-tipo | Até 3 palavras combinadas |
| 6 | Verificar respostas | GET /api/follow-up/mensagem/{mensagemId} | Lista todas as respostas |
Índice 0 → “Sim”
Índice 1 → “Não”
Índice 2 → “Talvez”
[1, 4] → “Sim Confirmo”
[1, 5] → “Sim Presença”
[2, 6] → “Não Ausência”
[1, 4, 5] → “Sim Confirmo Presença”
[3, 4] → “Talvez Confirmo”
Resposta Rápida: Botões diretos para respostas comuns
Flexibilidade: Combinação de até 3 palavras para respostas complexas
Estruturação: Respostas organizadas e categorizadas
Performance: Endpoints otimizados para cada tipo de resposta
Usabilidade: Interface intuitiva para diferentes perfis de usuário
Admin cria mensagem → Sistema gera um número único (ID) para cada pessoa que vai receber
Usuário recebe notificação na aplicação
Usuário clica na mensagem → Abre conteúdo completo com botões de resposta
Frontend carrega botões da tabela tiporesposta usando o ID 36
Usuário clica em “Sim” → Sistema registra resposta índice [0] = “Sim”
Sistema atualiza followup → Status muda para “Respondido: Sim”
Geração: ID é criado automaticamente pelo banco (IDENTITY/AUTO_INCREMENT)
Transmissão: ID é enviado junto com a notificação na aplicação
Obtenção: Frontend recebe o ID diretamente da notificação
Carregamento: Frontend usa ID para buscar opções da tabela tiporesposta
Resposta: Usuário clica em botão → Frontend envia índice para API
Atualização: Sistema atualiza followup com resposta correspondente
✅ Segurança: Cada usuário só pode responder seu próprio followup
✅ Simplicidade: Não precisa buscar ou calcular IDs
✅ Rastreabilidade: Sistema sabe exatamente quem respondeu o quê
✅ Flexibilidade: Mesmo usuário pode ter múltiplos followups para mensagens diferentes
🎉 Este fluxo demonstra como o sistema permite respostas estruturadas e flexíveis, mantendo a simplicidade para o usuário final enquanto oferece controle total para os administradores.
Versão: 1.0
Última Atualização: Outubro de 2025
Responsável: Equipe de Desenvolvimento CordenaAi