A little color. A little motion. A tiny experiment in CSS gradients.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Planilha — Serviço / Qtd / Valor</title>
<style>
:root{--indigo:#4f46e5;--violet:#7c3aed;--slate:#1e293b;--bg:#f1f5f9;--card:#fff;--line:#e2e8f0}
*{box-sizing:border-box;margin:0;padding:0;font-family:'Segoe UI',system-ui,sans-serif}
body{background:var(--bg);color:#0f172a;padding:24px}
.wrap{max-width:860px;margin:0 auto}
header{background:linear-gradient(135deg,var(--indigo),var(--violet));color:#fff;border-radius:18px 18px 0 0;padding:26px 32px;box-shadow:0 10px 30px rgba(79,70,229,.35)}
header h1{font-size:24px}
header p{opacity:.85;font-size:13px;margin-top:6px}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;padding:20px 24px;background:var(--card);border-bottom:1px solid var(--line)}
.card{border-radius:14px;padding:14px 16px;color:#fff;box-shadow:0 4px 14px rgba(0,0,0,.08)}
.card small{display:block;opacity:.9;font-size:11px;text-transform:uppercase;letter-spacing:.8px}
.card b{font-size:22px;display:block;margin-top:4px}
.c1{background:linear-gradient(135deg,#4f46e5,#6366f1)}
.c2{background:linear-gradient(135deg,#059669,#10b981)}
.c3{background:linear-gradient(135deg,#7c3aed,#a78bfa)}
.toolbar{display:flex;gap:10px;padding:16px 24px;background:var(--card);align-items:center;flex-wrap:wrap}
.toolbar .spacer{flex:1}
button{border:0;border-radius:10px;padding:10px 18px;font-size:13px;font-weight:600;cursor:pointer;color:#fff;transition:.15s transform;box-shadow:0 3px 10px rgba(0,0,0,.12)}
button:hover{transform:translateY(-1px)}
.btn-add{background:linear-gradient(135deg,#4f46e5,#7c3aed)}
.btn-xls{background:linear-gradient(135deg,#059669,#10b981)}
.btn-clear{background:linear-gradient(135deg,#dc2626,#f43f5e)}
.btn-save{background:linear-gradient(135deg,#0ea5e9,#38bdf8)}
#saveStat{font-size:12px;color:#64748b;white-space:nowrap}
.tablewrap{background:var(--card);padding:0 24px 24px;overflow-x:auto}
table{width:100%;border-collapse:separate;border-spacing:0;margin-top:4px}
th{background:var(--slate);color:#fff;font-size:12px;text-transform:uppercase;letter-spacing:.6px;padding:13px 10px}
th:first-child{border-radius:10px 0 0 0}th:last-child{border-radius:0 10px 0 0}
td{padding:0;border-bottom:1px solid var(--line);background:#fff}
tr:nth-child(even) td{background:#eef2ff}
input{width:100%;border:0;background:transparent;padding:12px 10px;font-size:14px;color:#0f172a;outline:none}
input:focus{background:#e0e7ff;box-shadow:inset 0 0 0 2px var(--indigo);border-radius:4px}
.num input{text-align:right}
.total{font-weight:700;text-align:right;padding:14px 10px}
.del{background:#fee2e2;color:#b91c1c;border-radius:8px;padding:6px 10px;font-size:12px;box-shadow:none}
footer{padding:14px 24px;background:var(--card);border-radius:0 0 18px 18px;color:#64748b;font-size:12px;text-align:center;border-top:1px solid var(--line)}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>📋 PLANILHA DE SERVIÇO — EDITÁVEL</h1>
<p>Serviço / Descrição • Qtd • Valor — clique em 💾 Salvar alterações para gravar</p>
</header>
<div class="cards">
<div class="card c1"><small>Itens</small><b id="k-qtd">0</b></div>
<div class="card c2"><small>Valor Total</small><b id="k-total">R$ 0,00</b></div>
<div class="card c3"><small>Soma das Qtd</small><b id="k-qty">0</b></div>
</div>
<div class="toolbar">
<button class="btn-save" onclick="saveChanges()">💾 Salvar alterações</button>
<span id="saveStat">✓ Pronto</span>
<div class="spacer"></div>
<button class="btn-add" onclick="addRow()">+ Nova linha</button>
<button class="btn-xls" onclick="exportExcel()">⬇ Excel</button>
<button class="btn-clear" onclick="limpar()">🗑 Limpar</button>
</div>
<div class="tablewrap">
<table>
<thead>
<tr>
<th style="width:55%">Serviço / Descrição</th>
<th style="width:15%" class="num">Qtd</th>
<th style="width:20%" class="num">Valor</th>
<th style="width:10%"></th>
</tr>
</thead>
<tbody id="tbody"></tbody>
<tfoot>
<tr>
<td style="text-align:right;font-weight:700;padding:14px 10px;background:#059669;color:#fff">💰 TOTAL GERAL</td>
<td class="total" style="background:#059669;color:#fff" id="f-qtd">0</td>
<td class="total" style="background:#059669;color:#fff" id="f-total">R$ 0,00</td>
<td style="background:#059669"></td>
</tr>
</tfoot>
</table>
</div>
<footer>Clique em 💾 Salvar alterações para gravar (Ctrl+S). Os dados ficam apenas no seu navegador.</footer>
</div>
<script>
const LS='planilha-servico-qtd-valor-v1';
const seed=[
{desc:'Troca de compressor hermético',qtd:1,val:850},
{desc:'Manutenção preventiva - câmara fria',qtd:3,val:120},
{desc:'Lavagem de serpentina + check',qtd:2,val:150},
{desc:'Carga de gás R404A',qtd:1,val:320},
{desc:'Troca de termostato digital',qtd:1,val:180},
{desc:'Limpeza de filtros e degelo',qtd:4,val:90}
];
let rows=[];
let dirty=false;
const fmt=n=>n.toLocaleString('pt-BR',{style:'currency',currency:'BRL'});
function load(){try{rows=JSON.parse(localStorage.getItem(LS))||null}catch(e){rows=null}
if(!Array.isArray(rows)||!rows.length)rows=seed.map(r=>({...r}))}
function save(){localStorage.setItem(LS,JSON.stringify(rows))}
function flash(msg){
const t=document.createElement('div');t.textContent=msg;
Object.assign(t.style,{position:'fixed',bottom:'20px',right:'20px',background:'#0f172a',color:'#fff',
padding:'12px 20px',borderRadius:'10px',zIndex:999,boxShadow:'0 8px 24px rgba(0,0,0,.3)',fontSize:'13px'});
document.body.appendChild(t);setTimeout(()=>t.remove(),1800);
}
function setStat(txt,color){const el=document.getElementById('saveStat');el.textContent=txt;el.style.color=color;}
function markDirty(){dirty=true;setStat('● Alterações não salvas','#d97706');}
function saveChanges(){
save();dirty=false;
setStat('✓ Salvo às '+new Date().toLocaleTimeString('pt-BR'),'#059669');
flash('💾 Alterações salvas!');
}
function render(){
const tb=document.getElementById('tbody');
tb.innerHTML=rows.map((r,i)=>`<tr>
<td><input value="${(r.desc||'').replace(/"/g,'"')}" placeholder="Descreva o serviço" oninput="upd(${i},'desc',this.value)"></td>
<td class="num"><input type="number" min="0" value="${r.qtd??''}" oninput="upd(${i},'qtd',this.value)"></td>
<td class="num"><input type="number" min="0" step="0.01" value="${r.val??''}" oninput="upd(${i},'val',this.value)"></td>
<td class="num"><button class="del" onclick="del(${i})">✕</button></td>
</tr>`).join('');
stats();
}
function upd(i,k,v){rows[i][k]=v;stats();markDirty()}
function addRow(){rows.push({desc:'',qtd:1,val:''});render();markDirty()}
function del(i){if(!confirm('Excluir esta linha?'))return;rows.splice(i,1);render();markDirty()}
function limpar(){if(!confirm('Limpar TODA a planilha?'))return;rows=[{desc:'',qtd:1,val:''}];render();markDirty()}
function stats(){
let total=0,qty=0,itens=0;
rows.forEach(r=>{total+=(parseFloat(r.qtd)||0)*(parseFloat(r.val)||0);
qty+=parseFloat(r.qtd)||0;if(r.desc)itens++});
document.getElementById('k-qtd').textContent=itens;
document.getElementById('k-total').textContent=fmt(total);
document.getElementById('k-qty').textContent=qty;
document.getElementById('f-qtd').textContent=qty;
document.getElementById('f-total').textContent=fmt(total);
}
function exportExcel(){
save();dirty=false;setStat('✓ Salvo às '+new Date().toLocaleTimeString('pt-BR'),'#059669');
let html=`<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"><head><meta charset="utf-8">
<style>td{mso-number-format:'\\@'} .n{mso-number-format:'\\#\\,##0\\.00'}</style></head><body><table border="1">
<tr><th style="background:#4f46e5;color:#fff">Serviço / Descrição</th><th style="background:#4f46e5;color:#fff">Qtd</th><th style="background:#4f46e5;color:#fff">Valor</th></tr>`;
rows.forEach(r=>{
html+=`<tr><td>${r.desc||''}</td><td class="n">${r.qtd||''}</td><td class="n">${r.val||''}</td></tr>`});
const total=rows.reduce((s,r)=>s+(parseFloat(r.qtd)||0)*(parseFloat(r.val)||0),0);
html+=`<tr><td style="font-weight:bold;text-align:right;background:#059669;color:#fff">TOTAL GERAL</td><td></td><td class="n" style="font-weight:bold;background:#059669;color:#fff">${total.toFixed(2)}</td></tr>`;
html+='</table></body></html>';
const blob=new Blob([''+html],{type:'application/vnd.ms-excel;charset=utf-8'});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
a.download='Planilha_Servicos.xls';a.click();URL.revokeObjectURL(a.href);
}
load();render();
document.addEventListener('keydown',e=>{
if((e.ctrlKey||e.metaKey)&&e.key==='s'){e.preventDefault();saveChanges()}
});
window.addEventListener('beforeunload',e=>{if(dirty){e.preventDefault();e.returnValue=''}});
</script>
</body>
</html>