Documentação — Como funciona
Última atualização: 6 de agosto de 2026
1. Visão geral do fluxo
Um compromisso é criado por um organizador com sessão iniciada, e depois enviado a um convidado através de um link público único. O convidado não precisa de conta: responde a partir de uma página pública, e a resposta surge de imediato no painel do organizador.
Organizador (com sessão iniciada)
└─ cria um compromisso ──▶ appointments (status = pending, invite_token gerado)
├─ «Enviar por email» ──▶ função de servidor sendInviteEmail ──▶ Resend
└─ «Copiar link» ──▶ /invite/<token>
Convidado (sem conta)
└─ abre /invite/<token> ──▶ RPC get_invite(_token) (leitura pública filtrada)
└─ Aceitar / Recusar ──▶ RPC respond_to_invite(_token, _accept, _message)
└─ status = accepted | declined, responded_at = now()
Organizador
└─ painel: badge de estado, data de resposta, mensagem do convidado, estatísticas2. Conteúdo do email de convite
O email é composto no servidor, na função sendInviteEmail e enviado através do Resend. Remetente: valor de RESEND_FROM (por defeito «Cadence <onboarding@resend.dev>»). Assunto: «Compromisso: {{title}}».
Estrutura do corpo, por ordem:
- Título «Proposta de compromisso».
- Saudação «Olá {{guestName}},».
- Bloco de mensagem personalizada (apenas organizadores Business).
- Título do compromisso a negrito, depois data e horário formatados de acordo com o idioma.
- Local e descrição, cada um mostrado apenas se preenchido.
- Botão de ação verde «Confirmar ou recusar» que aponta para o link de convite.
- Lembrete do link em texto simples, para clientes de email que bloqueiam botões.
- Bloco «Avaliações Google» (Business): nome da empresa, classificação, link de avaliações, link do Maps.
Todos os valores dinâmicos são escapados em HTML antes da inserção: nenhum conteúdo introduzido pelo organizador pode injetar markup no email.
Variáveis disponíveis
| Variável | Origem | Utilização |
|---|---|---|
| {{guestName}} | contacts.name | Linha de saudação «Olá {{guestName}},». Vazia se não houver contacto associado. |
| {{title}} | appointments.title | Assunto do email «Compromisso: {{title}}» e título a negrito no corpo. |
| {{dateRange}} | appointments.starts_at / ends_at | Formatado de acordo com o idioma: «terça-feira, 12 de agosto, 15:00 → 15:30» (Intl.DateTimeFormat). |
| {{location}} | appointments.location | Linha «Local: …». O bloco inteiro é omitido se o campo estiver vazio. |
| {{description}} | appointments.description | Parágrafo livre mostrado abaixo da data. Omitido se estiver vazio. |
| {{inviteLink}} | origin + /invite/ + appointments.invite_token | Botão «Confirmar ou recusar» + lembrete do link em texto simples no final do email (copiar/colar). |
| {{inviteMessage}} | profiles.invite_message (Business) | Caixa verde de mensagem personalizada, mostrada apenas para um organizador Business. |
| {{businessName}} / {{googleRating}} / {{googleReviewUrl}} / {{googleMapsUrl}} | profiles.* (Business) | Bloco «Avaliações Google» no rodapé do email e na página de convite, se a opção estiver ativada. |
Casos em que o envio não é concluído
- no_email : o contacto não tem endereço de email. O organizador tem de copiar o link e enviá-lo por outro canal.
- no_api_key : a chave de envio não está configurada. O link de convite é mesmo assim devolvido à interface para cópia manual.
- provider_error : recusa do fornecedor de email (endereço inválido, domínio não verificado…). O estado do compromisso mantém-se inalterado.
- Em caso de sucesso, invite_sent_at fica com data e hora registadas: este campo alimenta o acompanhamento «links enviados» nas estatísticas.
3. Ecrãs de confirmação (página /invite/<token>)
Página pública, sem autenticação, renderizada a partir da função protegida get_invite, que devolve apenas os campos necessários para a apresentação.
- A carregar : esqueleto animado (calendário) enquanto a resposta não chega.
- Link inválido ou expirado : mensagem de erro; não é revelada nenhuma informação sobre o organizador nem sobre o compromisso.
- Estado «pendente» : assunto, data e horário, duração, local, nome do organizador, mensagem personalizada (Business), campo livre opcional (500 caracteres máx.), depois dois botões «Aceito» e «Recuso».
- Após a aceitação : badge verde «Aceite», confirmação no ecrã, lembrete das informações práticas e bloco de avaliações Google se o organizador for Business.
- Após a recusa : badge vermelho «Recusado», mensagem a indicar que o organizador foi avisado; o convidado pode reabrir o link e alterar a sua resposta enquanto o compromisso não for cancelado.
- Compromisso cancelado : os botões de resposta deixam de ter efeito, o estado apresentado mantém-se «Cancelado».
A mensagem livre inserida pelo convidado é truncada a 500 caracteres do lado do cliente e a 1000 do lado da base de dados, e depois mostrada ao organizador na ficha do compromisso.
4. Estados dos compromissos
| Estado | Etiqueta | Gatilho | Efeito |
|---|---|---|---|
| pending | Pendente | Na criação do compromisso (valor por defeito na base de dados). | O link está ativo, o convidado pode responder. Contabilizado em «respostas esperadas». |
| accepted | Aceite | O convidado clica em «Aceito» — chamada a respond_to_invite(_accept = true). | responded_at e response_message são preenchidos; badge verde no painel. |
| declined | Recusado | O convidado clica em «Recuso» — respond_to_invite(_accept = false). | Mesmo registo que a aceitação, badge vermelho. O horário permanece visível para um lembrete. |
| cancelled | Cancelado | Ação do organizador a partir do seu painel. | O link deixa de aceitar respostas (respond_to_invite só atualiza os outros estados). |
5. Fluxo do lado do servidor
- Criação / edição / eliminação : efetuadas pelo cliente autenticado; o isolamento dos dados é garantido pelas regras de acesso da base de dados.
- sendInviteEmail: função de servidor protegida. Recarrega o compromisso filtrando pelo identificador do requerente, recusa se o compromisso não lhe pertencer, compõe o email, chama o Resend e depois regista a data/hora do envio.
- get_invite(_token): função da base de dados com privilégios elevados, única via de leitura pública. Devolve apenas os campos de apresentação e oculta as informações de marca para um organizador que não seja Business.
- respond_to_invite(_token, _accept, _message): atualiza o estado, a mensagem e a data/hora da resposta. Atua apenas na linha correspondente ao token e ignora compromissos cancelados.
- get_plan_usage(): devolve o plano, o consumo do mês e o indicador de administrador para a apresentação das quotas.
6. Lógica de autorização
- Organizador : acesso completo de leitura e escrita aos seus próprios contactos e compromissos, e apenas a eles.
- Convidado sem sessão iniciada : sem acesso direto às tabelas. Só pode ler um compromisso através do seu token e responder-lhe. O token é aleatório (36 caracteres hexadecimais) e não dá acesso a mais nada.
- Funções : armazenadas numa tabela dedicada (utilizador, moderador, administrador) e verificadas no servidor; nunca são lidas a partir do navegador para conceder um direito.
- Administrador : acesso à área /admin (todas as contas, contactos e compromissos) e isenção das quotas do plano Free-mium.
- Plano : a mudança de nível não pode ser forçada a partir do cliente; só o processo de pagamento Stripe ou um administrador podem alterá-la.
- Quotas : aplicadas na base de dados antes de cada criação — 10/15 (Free-mium), 30/50 (Basic), 50/100 (Standard), 250/500 (Economic) contactos/compromissos por mês de calendário, ilimitado no Business e para administradores.
7. Lista de verificação para a equipa editorial
- Alterar um texto de email: atuar sobre o template HTML da função de envio, não sobre a página de convite.
- Alterar um ecrã de confirmação: atuar sobre a página pública de convite.
- Qualquer nova variável tem de vir de um campo já exposto pela função de leitura pública.
- Nunca mostrar na página pública dados de outro compromisso ou de outro contacto.
- Testar sempre os três estados: pendente, aceite, recusado.