Relações: o histórico de interações
Começa hoje a segunda parte do módulo: as funcionalidades de gestão. Cada cliente passa a ter uma ficha com o histórico de chamadas, emails, reuniões e visitas — e, para isso, vais ligar duas tabelas com uma chave estrangeira.
🎯 No fim desta aula vais conseguir
- Explicar o que é uma relação 1 para N entre duas tabelas (um cliente → muitas interações).
- Usar uma chave estrangeira (
clienteId) para ligar cada interação ao seu cliente. - Abrir a ficha de um cliente e registar e apagar interações, que ficam guardadas no browser.
- Mostrar só as interações de um cliente, da mais recente para a mais antiga (filtrar + ordenar).
🧰 Antes de começar
- Abre a pasta
meu-crmno VS Code e oindex.htmlcom o Live Server. - Confirma que tens pelo menos 3 clientes na tabela. Se não tiveres, cria-os agora (inventa nomes de clientes do teu setor).
- Abre a Consola (F12) e confirma que não há erros vermelhos.
😕 O teu projeto não funciona ou faltaste às últimas aulas? Descarrega o ponto de controlo da aula 12, descompacta-o e trabalha a partir dele. Repara que o ponto de controlo não tem o campo Cidade da ficha da aula 12: se o teu projeto funciona, continua com o teu.
🧭 A segunda parte numa frase
Na primeira parte aprendeste a guardar, editar, apagar e pesquisar clientes. Agora vais transformar o CRM numa verdadeira aplicação de gestão. Até à aula 16 acrescentas as peças que (quase) todas as aplicações de gestão têm; nas aulas 17 a 20 adaptas o CRM ao teu setor, testas, publicas e apresentas.
| Aula | O que acrescentas ao CRM | Pergunta de negócio a que responde |
|---|---|---|
| 13 (hoje) | Histórico de interações de cada cliente | «O que é que já falámos com este cliente?» |
| 14 | Oportunidades de negócio por fases | «Que negócios temos em curso e em que ponto estão?» |
| 15 | Painel com indicadores | «Como está a correr o negócio?» |
| 16 | Relatórios para Excel e cópias de segurança | «Consigo tirar os dados daqui e não os perder?» |
Parte A — A ideia: duas tabelas ligadas (≈ 15 min)
💡 Porque é que um CRM precisa de um histórico?
Imagina que trabalhas numa empresa e o telefone toca: é a Ana Silva. Antes de falar com ela, querias saber: «Quando foi a última vez que falámos? O que lhe prometemos?» Sem histórico, cada colega começa do zero e o cliente sente que ninguém se lembra dele. Por isso, todos os CRM guardam as interações: cada chamada, email, reunião ou visita.
Analogia do escritório: antigamente havia um arquivador com uma ficha por cliente, cada uma com um número (o nº de cliente). Ao lado do telefone havia um caderno de contactos, onde se escrevia uma linha por cada contacto: «3 de outubro — Chamada — cliente nº 17 — pediu orçamento». No caderno não se copiava a morada nem o email do cliente: escrevia-se só o número. Quem quisesse saber mais ia ao arquivador procurar a ficha nº 17.
No nosso CRM vai ser igual: a tabela clientes é o arquivador e a nova tabela interacoes é o caderno.
Tabela clientes (o arquivador)
| id | nome | empresa |
|---|---|---|
| 17 | Ana Silva | Padaria Silva |
| 23 | Rui Costa | Oficina Costa |
Tabela interacoes (o caderno)
| id | clienteId | data | tipo |
|---|---|---|---|
| 101 | 17 | 2026-10-01 | Chamada |
| 102 | 23 | 2026-10-02 | |
| 103 | 17 | 2026-10-03 | Reunião |
💡 No teu CRM os id não são 17 ou 23, são números enormes como 1759486412345 (vêm do Date.now(), lembras-te da aula 8?). Aqui usamos números pequenos para ser mais fácil de ler.
Duas palavras novas, muito usadas em bases de dados:
- Relação 1 para N (lê-se «um para ene»): um cliente pode ter muitas (N) interações, mas cada interação pertence a um só cliente. Outros exemplos: um aluno tem muitas faltas; uma fatura tem muitas linhas; um cliente tem muitas encomendas.
- Chave estrangeira: o campo
clienteIdde uma interação guarda oid(a chave primária) de um cliente. Chama-se «estrangeira» porque é a chave de outra tabela. Na tabela acima, as interações 101 e 103 são da Ana (cliente 17).
Porque guardamos só o número, e não o nome do cliente? (1) Se a Ana mudar de email, só se corrige num sítio; (2) pode haver duas clientes chamadas «Ana Silva», mas nunca dois clientes com o mesmo id; (3) um número ocupa pouco espaço.
É assim que funcionam as bases de dados relacionais que viste na aula 11.
Em SQL, mostrar o histórico da Ana seria assim (não é para colar, é só para comparar):
Hoje vais fazer exatamente isto, mas em JavaScript.
🤔 Pensa: se a ficha nº 17 for deitada fora do arquivador, o que acontece às linhas do caderno que dizem «cliente nº 17»? Escreve a tua resposta no caderno — vais investigá-lo no fim da aula.
Parte B — A ficha do cliente com o histórico (≈ 45 min)
1 A ficha do cliente (HTML)
Primeiro, a parte que se vê: uma secção nova com o nome do cliente, um pequeno formulário para registar interações e uma lista vazia, onde o JavaScript vai escrever o histórico. Repara em três coisas:
- O atributo
hidden(escondido) na<section>: a ficha começa escondida e só aparece quando escolheres um cliente. - O
<input type="date">mostra um calendário para escolher a data. - A lista
<ul id="lista-interacoes">está vazia: os itens<li>vão ser criados pelo JavaScript, tal como as linhas da tabela de clientes.
No index.html, procura o comentário AULA 13: COLAR AQUI A FICHA DO CLIENTE. Fica depois do </section> que fecha a secção dos clientes — não coles a ficha dentro da secção dos clientes.
🤔 Prevê: grava e olha para o CRM no browser. O que mudou?
✅ Deves ver: nada de novo! A ficha existe, mas está escondida por causa do hidden. Para confirmares que está lá: botão direito → Inspecionar e procura <section id="detalhe" class="cartao" hidden> no separador Elements.
2 As funções da ficha (JavaScript)
⚠️ Atenção ao sítio: no app.js há dois comentários com «AULA 13: COLAR AQUI»: um pequeno, dentro da função atualizarEcra() (é para o passo 4), e um grande, com =====, perto do fim do ficheiro.
Este bloco vai para o grande: // ===== AULA 13: COLAR AQUI =====. Usa Ctrl+F para o encontrar.
O bloco é comprido, mas cada função faz uma coisa simples:
let interacoes = carregar('interacoes');— a tabela nova. A funçãocarregardodados.js(aula 8) serve para qualquer tabela: os dados vão ficar na chavecrm_interacoes. Repara que não precisamos de mexer nodados.js— é a vantagem de ter uma camada de acesso a dados.let clienteAberto = null;— lembra-se de qual é o cliente cuja ficha está aberta.nullquer dizer «nenhum».verCliente(id)— abre a ficha: escreve o nome e os dados do cliente, põe a data de hoje no calendário, mostra a secção (hidden = false), desenha o histórico e desliza a página até à ficha.fecharDetalhe()— esconde a ficha e volta a pôrclienteAberto = null.desenharInteracoes()— faz duas coisas: filtra (só as interações comclienteId === clienteAberto, como oWHERE) e ordena da data mais recente para a mais antiga (como oORDER BY data DESC). Depois cria um<li>para cada interação.guardarInteracao()— cria o objeto da interação. A linhaclienteId: clienteAbertoé a chave estrangeira: é ela que liga a interação ao cliente.apagarInteracao(id)— igual à ideia doapagarClienteda aula 9: pede confirmação, procura a posição com um ciclo e tira-a comsplice. Obreakpára o ciclo assim que encontra.
💡 Na ordenação, (a, b) => b.data.localeCompare(a.data) tem o b primeiro: é por isso que fica do mais recente para o mais antigo (lembras-te do «Nome Z → A» da aula 10?).
✅ Deves ver: ainda nada de novo no ecrã, e nenhum erro na Consola. Experimenta escrever na Consola interacoes (deve aparecer [], uma lista vazia) e clienteAberto (deve aparecer null).
3 O botão 👁️ «Ver ficha»
Agora falta uma maneira de abrir a ficha. Na função linhaCliente(), dentro da célula das ações, está o comentário <!-- AULA 13: COLAR AQUI O BOTÃO VER --> que deixaste ficar na aula 9.
Cola o botão por baixo desse comentário (fica antes do botão ✏️).
Este botão é HTML escrito dentro do JavaScript (dentro das crases ` `). O ${cliente.id} é trocado pelo id de cada cliente, por isso cada botão sabe qual é a ficha que deve abrir.
✅ Deves ver: cada linha da tabela tem agora um botão 👁️. Ao clicares, aparece a ficha por baixo da tabela, com o nome do cliente, a empresa, o email, o telefone e o estado, a data de hoje no calendário e a frase «Ainda não há interações com este cliente.»
😕 Não aparece o botão / aparece um erro
- Erro
Unexpected token '<': colaste o botão fora das crases. Tem de ficar entre<td class="acoes">e</td>, junto aos botões ✏️ e 🗑️. - Ao clicar:
verCliente is not defined→ o bloco do passo 2 não foi gravado, ou foi colado dentro da funçãoatualizarEcra()(no comentário pequeno). Muda-o para o comentário grande. - Ao clicar:
Cannot set properties of null (setting 'textContent')→ falta a ficha do passo 1, ou colaste-a no ficheiro errado (tem de ir para oindex.html). - Ao clicar:
procurarCliente is not defined→ o teu projeto não tem a aula 9. Usa o ponto de controlo da aula 12.
4 Atualizar a ficha sempre que os dados mudam
As funções guardarInteracao() e apagarInteracao() terminam com atualizarEcra(), a função que volta a desenhar tudo (aula 7). Falta dizer-lhe que também tem de desenhar o histórico.
Cola esta linha dentro da função atualizarEcra(), por baixo do comentário pequeno // AULA 13: COLAR AQUI.
🤔 Prevê: se te esquecesses desta linha, o que acontecia quando registasses uma interação?
Confere: a tua função deve ficar assim.
💡 Resposta ao «prevê»: a interação ficava guardada, mas só a vias depois de fechar e voltar a abrir a ficha.
5 Testar
- Grava todos os ficheiros e clica no 👁️ de um cliente.
- Regista uma interação: tipo Chamada, descrição «Pediu orçamento», com a data de hoje.
- Regista uma segunda interação, mas muda a data para a semana passada. Deve aparecer por baixo da primeira (a mais recente fica em cima).
- Tenta registar uma interação sem descrição: deve aparecer o aviso «Escreve uma descrição da interação.»
- Carrega em F5, volta a abrir a ficha do mesmo cliente: as interações continuam lá.
- Abre a ficha de outro cliente: não deve ter nenhuma das interações do primeiro.
- Apaga uma interação com o 🗑️ (experimenta primeiro Cancelar, depois OK). Por fim, clica em Fechar ✖.
✅ Deves ver: cada cliente com o seu histórico, da data mais recente para a mais antiga; os dados continuam lá depois de recarregar a página.
😕 Não funciona?
- Registas a interação mas a lista não muda (só aparece se fechares e abrires a ficha) → falta o passo 4.
Cannot access 'interacoes' before initialization(ou'clienteAberto') → colaste o bloco do passo 2 por baixo da linha de arranqueatualizarEcra();, ou colaste a linha do passo 4 fora da função. A linha de arranque tem de ser sempre a última do ficheiro.Identifier 'interacoes' has already been declared→ colaste o bloco do passo 2 duas vezes. Apaga uma das cópias.- A ficha aparece no meio da secção dos clientes → no passo 1 colaste antes do
</section>dos clientes. Corta e cola no sítio certo. - Carregas em «Registar interação» e não acontece nada → abre a Consola e lê o erro:
guardarInteracao is not definedquer dizer que o passo 2 não está no sítio certo.
🔎 Investiga
- No DevTools, abre o separador Aplicação (Application) → Armazenamento local → o endereço do Live Server. Clica em
crm_interacoese depois emcrm_clientes. Compara oclienteIdde uma interação com oiddo cliente a quem a registaste. São iguais? É isto uma chave estrangeira. - Cria um cliente «Teste Apagar», regista-lhe 2 interações e depois apaga o cliente. Volta a
crm_interacoes: as interações dele ainda lá estão? A que cliente pertencem agora? (Chama-se a isto registos órfãos — o desafio ⭐ resolve o problema.) - Com uma ficha aberta, escreve
clienteAbertona Consola. Fecha a ficha e escreve outra vez. O que mudou? Porquê? - Porque guardamos a data como
2026-10-03e não como03/10/2026? Ordena «como se fossem palavras» (letra a letra, da esquerda para a direita) estas três datas:03/10/2026,28/09/2026,15/01/2027. A ordem fica certa? E com2026-10-03,2026-09-28,2027-01-15?
✍️ Exercícios (fazem parte do teu projeto)
Estes exercícios não têm o código pronto: usa o que aprendeste hoje e nas aulas anteriores.
E1 · Um ícone para cada tipo de interação
No histórico, antes do tipo de interação, mostra um ícone: 📞 Chamada, ✉️ Email, 🤝 Reunião e 🚗 Visita.
💡 Dica: cria uma função (por exemplo iconeTipo(tipo)) que recebe o tipo e devolve (return) o ícone certo, usando if / else if (aula 5). Depois, em desenharInteracoes(), procura a linha que mostra ${interacao.tipo} e chama lá a tua função com ${…}. Podes copiar os ícones desta página.
E2 · Contador no título
O título do histórico deve mostrar quantas interações tem o cliente aberto, por exemplo «Histórico de interações (3)». O número tem de mudar quando registas ou apagas uma interação.
💡 Dica: no index.html, põe um <span id="…"> dentro do <h3>, à volta do número. Em desenharInteracoes(), há uma lista que tem só as interações deste cliente — qual é? Que propriedade de um array diz quantos elementos ele tem (aula 7)? Usa textContent para escrever o número.
⭐ Desafio · Apagar em cascata
Quando apagas um cliente, apaga também todas as interações dele (para não ficarem órfãs). Se a ficha desse cliente estiver aberta, fecha-a.
💡 Dica: em apagarCliente(), depois de guardar os clientes, cria uma lista nova (por exemplo restantes) e, com um ciclo, põe lá só as interações cujo clienteId é diferente (!==) do id apagado. Depois troca a lista interacoes pela nova e grava-a com guardar(…). Nas bases de dados a sério isto chama-se ON DELETE CASCADE.
📦 O que mostrar ao professor
⏱️ Se sobrar tempo
- Faz o desafio ⭐.
- Acrescenta um quinto tipo de interação ao
<select id="int-tipo">(por exemplo WhatsApp) e, se fizeste o E1, um ícone para ele (💬). - Na ficha, mostra também o NIF e as notas do cliente (em
verCliente(), na linha dodetalhe-info). - Pensa no teu setor: que tipos de interação fariam sentido? (Num ginásio: «Aula experimental», «Avaliação física»…) Aponta-os — vão ser úteis na aula 17.
🤖 Pedir ajuda à IA (opcional)
Se não percebeste bem a ideia de tabelas relacionadas, pede a uma IA gratuita que te explique com um exemplo do teu setor. Copia este pedido, troca o texto entre [ ] e cola-o na IA:
💡 Se for um erro vermelho na Consola, usa o pedido da aula 7: cola o erro e pede uma explicação, sem a solução.
📖 Palavras novas
- Interação
- Cada contacto com um cliente: chamada, email, reunião, visita…
- Relação 1 para N
- Ligação entre duas tabelas em que um registo de uma (um cliente) corresponde a muitos registos da outra (muitas interações).
- Chave primária
- O campo que identifica cada registo sem repetições (o nosso
id). - Chave estrangeira
- Campo que guarda a chave primária de outra tabela, para ligar os registos (o nosso
clienteId). - Registo órfão
- Registo cuja chave estrangeira aponta para algo que já não existe (ex.: interação de um cliente apagado).
- Apagar em cascata
- Quando se apaga um registo, apagam-se também os registos que dependem dele.
null- Valor que quer dizer «nada» ou «nenhum», de propósito.
hidden- Atributo HTML que esconde um elemento. Em JavaScript:
elemento.hidden = trueesconde,falsemostra. - Ordem descendente (DESC)
- Do maior para o menor (nas datas: da mais recente para a mais antiga).