Chiffrage BTP — Déboursé Sec
:root {
--bg: #0f0f0f;
--surface: #1a1a1a;
--surface2: #222222;
--border: #2e2e2e;
--accent: #f0c040;
--accent2: #e87040;
--text: #f0ece4;
--muted: #888;
--green: #5dbb7a;
--red: #e05555;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: 'DM Mono', monospace;
min-height: 100vh;
padding: 0 0 80px 0;
}
header {
background: var(--surface);
border-bottom: 2px solid var(--accent);
padding: 20px 28px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
}
header h1 {
font-family: 'Syne', sans-serif;
font-weight: 800;
font-size: 1.3rem;
letter-spacing: 0.05em;
color: var(--accent);
}
header span {
font-size: 0.75rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.1em;
}
.container { max-width: 960px; margin: 0 auto; padding: 24px 16px; }
/* Corps d'état selector */
.tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 24px;
}
.tab {
padding: 7px 14px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--muted);
font-family: 'DM Mono', monospace;
font-size: 0.78rem;
cursor: pointer;
transition: all 0.15s;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.tab:hover { border-color: var(--accent); color: var(--accent); }
.tab.active { background: var(--accent); color: #0f0f0f; border-color: var(--accent); font-weight: 600; }
/* Section */
.section-label {
font-family: 'Syne', sans-serif;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--muted);
margin-bottom: 10px;
}
/* Table chiffrage */
.chiffrage-table {
width: 100%;
border-collapse: collapse;
background: var(--surface);
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
}
.chiffrage-table thead tr {
background: var(--surface2);
border-bottom: 2px solid var(--accent);
}
.chiffrage-table th {
padding: 10px 12px;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
text-align: left;
font-weight: 500;
}
.chiffrage-table th:last-child { text-align: right; }
.chiffrage-table td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
font-size: 0.82rem;
vertical-align: middle;
}
.chiffrage-table tr:last-child td { border-bottom: none; }
.chiffrage-table tr:hover td { background: #1e1e1e; }
input[type="number"], input[type="text"] {
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
font-family: 'DM Mono', monospace;
font-size: 0.82rem;
padding: 5px 8px;
border-radius: 4px;
width: 100%;
transition: border-color 0.15s;
}
input[type="number"]:focus, input[type="text"]:focus {
outline: none;
border-color: var(--accent);
}
input[type="number"] { text-align: right; }
.col-designation { width: 28%; }
.col-unite { width: 7%; }
.col-qte { width: 9%; }
.col-mo { width: 11%; }
.col-fourniture { width: 11%; }
.col-taux { width: 10%; }
.col-ds { width: 11%; text-align: right; }
.col-total { width: 13%; text-align: right; }
.ds-val { color: var(--accent2); font-weight: 500; }
.total-val { color: var(--accent); font-weight: 600; }
/* Add row button */
.btn-add {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: transparent;
border: 1px dashed var(--border);
color: var(--muted);
font-family: 'DM Mono', monospace;
font-size: 0.78rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
margin-bottom: 28px;
}
.btn-add:hover { border-color: var(--accent); color: var(--accent); }
.btn-del {
background: none;
border: none;
color: var(--red);
cursor: pointer;
font-size: 1rem;
padding: 2px 6px;
border-radius: 3px;
opacity: 0.5;
transition: opacity 0.15s;
}
.btn-del:hover { opacity: 1; }
/* Récap */
.recap {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px 24px;
margin-top: 16px;
}
.recap-title {
font-family: 'Syne', sans-serif;
font-weight: 700;
font-size: 1rem;
color: var(--accent);
margin-bottom: 16px;
letter-spacing: 0.05em;
}
.recap-rows { display: flex; flex-direction: column; gap: 6px; }
.recap-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-radius: 4px;
background: var(--surface2);
font-size: 0.83rem;
}
.recap-row .corps { color: var(--text); }
.recap-row .montant { color: var(--accent); font-weight: 600; }
.recap-total {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 14px;
margin-top: 10px;
background: var(--accent);
border-radius: 4px;
font-family: 'Syne', sans-serif;
font-weight: 800;
font-size: 1.05rem;
color: #0f0f0f;
}
/* Coefficients */
.coeff-bar {
display: flex;
gap: 12px;
flex-wrap: wrap;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 20px;
margin-bottom: 24px;
align-items: center;
}
.coeff-item { display: flex; align-items: center; gap: 8px; font-size: 0.8rem; }
.coeff-item label { color: var(--muted); white-space: nowrap; }
.coeff-item input { width: 70px; }
.coeff-sep { color: var(--border); font-size: 1.2rem; }
.taux-ho {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
color: var(--muted);
}
.taux-ho input { width: 80px; }
/* Export */
.btn-export {
margin-top: 20px;
padding: 10px 24px;
background: var(--accent);
color: #0f0f0f;
font-family: 'Syne', sans-serif;
font-weight: 700;
font-size: 0.85rem;
border: none;
border-radius: 4px;
cursor: pointer;
letter-spacing: 0.05em;
transition: opacity 0.15s;
}
.btn-export:hover { opacity: 0.85; }
@media (max-width: 600px) {
.col-mo, .col-fourniture, .col-taux { display: none; }
.chiffrage-table th.col-mo,
.chiffrage-table th.col-fourniture,
.chiffrage-table th.col-taux { display: none; }
}
⬛ CHIFFRAGE BTP
Déboursé sec — Tous corps d'état
Paramètres globaux
|
|
×1.265
Corps d'état
RÉCAPITULATIF GÉNÉRAL
TOTAL DÉBOURSÉ SEC
0,00 €
TOTAL PRIX DE VENTE HT
0,00 €
const CORPS = [
{ id: 'GO', label: 'Gros Œuvre', color: '#c0783a' },
{ id: 'ET', label: 'Étanchéité', color: '#5a8fc0' },
{ id: 'CH', label: 'Charpente', color: '#8b6fa0' },
{ id: 'CO', label: 'Couverture', color: '#5dbb7a' },
{ id: 'ME', label: 'Menuiserie', color: '#d4a020' },
{ id: 'PL', label: 'Plâtrerie', color: '#a0a0a0' },
{ id: 'CA', label: 'Carrelage', color: '#60b0a0' },
{ id: 'PE', label: 'Peinture', color: '#e07070' },
{ id: 'EL', label: 'Électricité', color: '#f0c040' },
{ id: 'PB', label: 'Plomberie', color: '#4090d0' },
{ id: 'CVC', label: 'CVC', color: '#f08040' },
{ id: 'VRD', label: 'VRD', color: '#80a060' },
];
// Data: { corpsId: [ {designation, unite, qte, tmo, pfourniture}, ... ] }
let data = {};
let activeCorps = 'GO';
// Default rows per corps
const defaultRows = () => [
{ designation: '', unite: 'm²', qte: 0, tmo: 0, pfourniture: 0 },
];
CORPS.forEach(c => { data[c.id] = defaultRows(); });
// Add some example rows
data['GO'] = [
{ designation: 'Maçonnerie parpaings 20cm', unite: 'm²', qte: 50, tmo: 1.5, pfourniture: 18 },
{ designation: 'Dalle béton e=12cm', unite: 'm²', qte: 80, tmo: 0.8, pfourniture: 22 },
];
data['PE'] = [
{ designation: 'Peinture murs (2 couches)', unite: 'm²', qte: 120, tmo: 0.3, pfourniture: 4.5 },
{ designation: 'Enduit plâtre', unite: 'm²', qte: 80, tmo: 0.6, pfourniture: 3 },
];
data['EL'] = [
{ designation: 'Tableau électrique 3x18', unite: 'ens.', qte: 1, tmo: 8, pfourniture: 320 },
{ designation: 'Prise de courant 2P+T', unite: 'u', qte: 20, tmo: 0.5, pfourniture: 8 },
];
function getTauxMO() { return parseFloat(document.getElementById('tauxMO').value) || 0; }
function getCoeffFG() { return parseFloat(document.getElementById('coeffFG').value) || 1; }
function getCoeffBen() { return parseFloat(document.getElementById('coeffBen').value) || 1; }
function getCoeffTotal() { return getCoeffFG() * getCoeffBen(); }
function fmt(n) {
return n.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €';
}
function calcDS(row) {
const mo = (row.tmo || 0) * getTauxMO();
const fourniture = parseFloat(row.pfourniture) || 0;
return mo + fourniture;
}
function calcTotal(row) {
return calcDS(row) * parseFloat(row.qte || 0);
}
function renderTabs() {
const el = document.getElementById('tabs');
el.innerHTML = CORPS.map(c => `
`).join('');
}
function setCorps(id) {
activeCorps = id;
renderTabs();
renderTable();
}
function renderTable() {
const rows = data[activeCorps];
const corps = CORPS.find(c => c.id === activeCorps);
const container = document.getElementById('tableContainer');
container.innerHTML = `
■ ${corps.label}
| Désignation |
Unité |
Qté |
T.MO (h/u) |
Fourniture (€/u) |
DS (€/u) |
Total DS (€) |
|
${rows.map((r, i) => renderRow(r, i)).join('')}
`;
}
function renderRow(r, i) {
const ds = calcDS(r);
const total = calcTotal(r);
return `
|
|
|
|
|
|
${fmt(ds)} |
${fmt(total)} |
|
`;
}
function updateRow(i, field, value) {
data[activeCorps][i][field] = field === 'designation' || field === 'unite' ? value : parseFloat(value) || 0;
refreshRowCalcs(i);
updateRecap();
}
function refreshRowCalcs(i) {
const row = data[activeCorps][i];
const ds = calcDS(row);
const total = calcTotal(row);
const tbody = document.getElementById('tbody');
if (!tbody) return;
const tr = tbody.children[i];
if (!tr) return;
tr.querySelector('.ds-val').textContent = fmt(ds);
tr.querySelector('.total-val').textContent = fmt(total);
}
function addRow() {
data[activeCorps].push({ designation: '', unite: 'm²', qte: 0, tmo: 0, pfourniture: 0 });
renderTable();
updateRecap();
}
function deleteRow(i) {
data[activeCorps].splice(i, 1);
if (data[activeCorps].length === 0) data[activeCorps] = defaultRows();
renderTable();
updateRecap();
}
function recalcAll() {
// Update coeff display
document.getElementById('coeffTotal').textContent = '×' + getCoeffTotal().toFixed(3);
renderTable();
updateRecap();
}
function getCorpsTotal(id) {
return (data[id] || []).reduce((sum, r) => sum + calcTotal(r), 0);
}
function updateRecap() {
const recapRows = document.getElementById('recapRows');
let totalDS = 0;
recapRows.innerHTML = CORPS.map(c => {
const t = getCorpsTotal(c.id);
totalDS += t;
if (t === 0) return '';
return `
■ ${c.label}
${fmt(t)}
`;
}).join('');
document.getElementById('recapTotalDS').textContent = fmt(totalDS);
document.getElementById('recapTotalPV').textContent = fmt(totalDS * getCoeffTotal());
}
function exportCSV() {
let csv = 'Corps d\'état;Désignation;Unité;Quantité;T.MO (h/u);Fourniture (€/u);DS unitaire (€);Total DS (€)\n';
CORPS.forEach(c => {
(data[c.id] || []).forEach(r => {
if (!r.designation && r.qte === 0) return;
const ds = calcDS(r);
const total = calcTotal(r);
csv += `${c.label};${r.designation};${r.unite};${r.qte};${r.tmo};${r.pfourniture};${ds.toFixed(2)};${total.toFixed(2)}\n`;
});
});
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'chiffrage_btp.csv';
a.click();
}
// Init
renderTabs();
renderTable();
recalcAll();