Projeto integrador: calculadora de orçamento
Consolidamos lógica, DOM e organização em utilitário real: estimativa de custo de projeto de software por perfil (dev, design, QA). Escopo fechado, código legível, testes manuais documentados.
Requisitos
- Linhas por perfil: horas × valor hora.
- Desconto 8% se total de horas > 200.
- Validação: horas e valores não negativos.
- UI: formulário + breakdown + total.
- Persistir último cálculo em localStorage.
Módulo de cálculo (puro)
const DESCONTO_LIMITE_HORAS = 200;
const DESCONTO_PERCENTUAL = 0.08;
function calcularOrcamento(linhas) {
if (!Array.isArray(linhas) || linhas.length === 0) {
throw new Error('Informe ao menos uma linha');
}
const subtotais = linhas.map(({ perfil, horas, valorHora }) => {
if (!perfil?.trim()) throw new Error('Perfil obrigatório');
if (horas < 0 || valorHora < 0) throw new Error(`Valores inválidos: ${perfil}`);
return { perfil, horas, valorHora, total: horas * valorHora };
});
const totalHoras = subtotais.reduce((s, l) => s + l.horas, 0);
let total = subtotais.reduce((s, l) => s + l.total, 0);
let desconto = 0;
if (totalHoras > DESCONTO_LIMITE_HORAS) {
desconto = total * DESCONTO_PERCENTUAL;
total -= desconto;
}
return { subtotais, totalHoras, total, desconto };
}
Integração DOM
function lerLinhasDoFormulario() {
return [...document.querySelectorAll('[data-linha]')].map(row => ({
perfil: row.querySelector('[name="perfil"]').value,
horas: Number(row.querySelector('[name="horas"]').value),
valorHora: Number(row.querySelector('[name="valorHora"]').value)
}));
}
function renderResultado(resultado) {
const alvo = document.querySelector('#resultado');
alvo.innerHTML = resultado.subtotais.map(l =>
`<p><strong>${l.perfil}</strong>: ${l.horas}h × R$ ${l.valorHora} = R$ ${l.total.toFixed(2)}</p>`
).join('');
alvo.innerHTML += `<p>Total horas: ${resultado.totalHoras}</p>`;
if (resultado.desconto) {
alvo.innerHTML += `<p>Desconto: R$ ${resultado.desconto.toFixed(2)}</p>`;
}
alvo.innerHTML += `<p><strong>Total: R$ ${resultado.total.toFixed(2)}</strong></p>`;
}
localStorage
function salvar(resultado) {
localStorage.setItem('orcamento:v1', JSON.stringify(resultado));
}
function restaurar() {
const raw = localStorage.getItem('orcamento:v1');
if (raw) renderResultado(JSON.parse(raw));
}
Checklist de entrega
- Funciona com 1 e 5 linhas de perfil.
- Erro claro para horas negativas.
- 200h exatas: sem desconto; 201h: com desconto.
- Recarregar página restaura último resultado.
- Código de cálculo isolado em arquivo separado do DOM.
Estrutura de arquivos sugerida
orcamento/
index.html ← markup sem lógica de negócio
styles.css ← layout e componentes
calculadora.js ← calcularOrcamento (puro, testável no Node)
app.js ← DOM, eventos, localStorage
README.md ← como rodar, casos de teste, decisões
Markup inicial (esqueleto)
<main>
<h1>Calculadora de orçamento</h1>
<form id="form-orcamento">
<div id="linhas">
<div data-linha>
<input name="perfil" placeholder="Perfil" required>
<input name="horas" type="number" min="0" step="1" required>
<input name="valorHora" type="number" min="0" step="0.01" required>
</div>
</div>
<button type="button" id="btn-add-linha">+ Linha</button>
<button type="submit">Calcular</button>
</form>
<section id="resultado" aria-live="polite"></section>
</main>
Implementação passo a passo
- Dia 1:
calcularOrcamentono Node com console.log — sem HTML. - Dia 2: formulário estático + submit chama função e exibe resultado em texto.
- Dia 3: adicionar/remover linhas dinamicamente; validação visual de erros.
- Dia 4: localStorage + polish CSS + README com matriz de testes.
Matriz de testes manuais
| Cenário | Entrada | Esperado |
|---|---|---|
| Vazio | submit sem linhas | Mensagem de erro |
| Negativo | horas = -1 | Erro por perfil |
| Limite desconto | 200h totais | Sem desconto |
| Acima limite | 201h totais | 8% sobre total |
| Persistência | calcular → F5 | Resultado restaurado |
Extensões opcionais (se sobrar tempo)
- Exportar resultado como JSON ou CSV.
- Modo escuro via
prefers-color-scheme. - Testes automatizados de
calcularOrcamentocom Node (sem framework,assertnativo).
Este projeto vira peça de portfolio — publique na semana 4 com GitHub Pages.
Para aprofundar na web
Para entender melhor este tema, pesquise por:
- "separar lógica apresentação JavaScript" — módulos puros vs DOM
- "localStorage API MDN" — persistência no navegador
- "validação formulário HTML5 JavaScript" — camadas cliente e regras de negócio
- "Node assert testar função pura" — testes mínimos sem framework
- "README projeto portfolio GitHub exemplo" — documentar para recrutadores
Grave GIF curto ou screenshot do projeto funcionando — evidência visual no portfolio.
Atividades
Por que isolar calcularOrcamento() do DOM?
Ver resposta
Resposta correta: B) Testar regras de negócio sem navegador
Lógica pura é testável no Node e reutilizável.
Com 250 horas totais, desconto incide sobre:
Ver resposta
Resposta correta: B) Total monetário agregado
Regra aplica percentual sobre soma dos subtotais.
Cite três casos de teste manual obrigatórios antes de considerar o projeto pronto.
Ver resposta
Horas negativas; limite 200 vs 201 horas; formulário vazio; persistência após reload; múltiplos perfis.
0 comments