Files
3d-pricing/templates/new_job.html
T

234 lines
12 KiB
HTML

{% extends 'base.html' %}
{% block title %}Nouveau calcul{% endblock %}
{% block content %}
<div class="page-header">
<h1><i class="bi bi-plus-circle me-2" style="color:#ff6b35"></i>Nouveau calcul de prix</h1>
</div>
<div class="row g-4">
<div class="col-lg-7">
<!-- Import 3mf -->
<div class="card mb-3 border border-warning border-opacity-50">
<div class="card-header py-3">
<i class="bi bi-file-earmark-zip text-warning me-2"></i>Import Bambu Studio (.3mf)
<span class="badge bg-warning text-dark ms-1">Optionnel</span>
</div>
<div class="card-body">
<div class="d-flex align-items-center gap-3">
<input type="file" id="file3mf" accept=".3mf" class="form-control" style="max-width:320px">
<button type="button" class="btn btn-outline-warning" onclick="parse3mf()">
<i class="bi bi-magic me-1"></i>Extraire
</button>
<span id="parseStatus" class="text-muted small"></span>
</div>
<div class="form-text mt-1">Glissez votre fichier .3mf depuis Bambu Studio pour remplir poids et duree automatiquement.</div>
</div>
</div>
<form method="POST" id="jobForm">
<div class="card mb-3">
<div class="card-header py-3">Donnees impression</div>
<div class="card-body">
<div class="row g-3 mb-3">
<div class="col-md-8">
<label class="form-label fw-semibold">Nom de la commande</label>
<input type="text" name="name" id="jobName" class="form-control" required placeholder="ex: Marqueurs competition salle X">
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Fichier source</label>
<input type="text" name="source_file" id="sourceFile" class="form-control" placeholder="nom_fichier.3mf">
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Client</label>
<select name="client_id" id="clientSelect" class="form-select">
<option value="">Sans client</option>
{% for c in clients %}
<option value="{{ c.id }}" data-dm="{{ c.design_multiplier }}">{{ c.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Matiere</label>
<select name="material_id" id="materialSelect" class="form-select">
<option value="">Prix global (parametres)</option>
{% for m in materials %}
<option value="{{ m.id }}" data-price="{{ m.price_per_kg }}" data-stock="{{ m.stock_g }}" data-threshold="{{ m.low_stock_threshold_g }}">
{{ m.name }} ({{ m.type }}) - {{ m.price_per_kg }}EUR/kg
{% if m.stock_g <= 0 %}[RUPTURE]{% elif m.stock_g <= m.low_stock_threshold_g %}[stock bas]{% endif %}
</option>
{% endfor %}
</select>
<div id="stockInfo" class="form-text"></div>
</div>
</div>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label fw-semibold">Poids filament (g)</label>
<input type="number" name="weight_g" id="weight_g" class="form-control" step="0.01" min="0.1" required placeholder="423.97">
<div class="form-text">Champ Modele dans Bambu</div>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Heures</label>
<input type="number" name="hours" id="hours" class="form-control" min="0" value="0">
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Minutes</label>
<input type="number" name="minutes" id="minutes" class="form-control" min="0" max="59" value="0">
<div class="form-text">Temps total Bambu</div>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header py-3">Parametres tarifaires</div>
<div class="card-body">
<div class="mb-4">
<label class="form-label fw-semibold">
Coefficient design <span class="badge bg-secondary ms-1" id="dmLabel">x1.80</span>
</label>
<div class="d-flex gap-2 mb-2">
<button type="button" class="btn btn-sm btn-outline-success" onclick="setDM(0.80)">STL fourni (x0.80)</button>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="setDM(1.80)">Creation complete (x1.80)</button>
</div>
<input type="number" name="design_multiplier" id="design_multiplier" class="form-control form-control-sm" step="0.05" min="0" value="1.80">
</div>
<div class="mb-4">
<label class="form-label fw-semibold d-flex justify-content-between">
Marge brute <span id="marginDisplay" class="text-muted">27.5 %</span>
</label>
<input type="range" name="gross_margin_pct" id="gross_margin_pct" class="form-range" min="20" max="35" step="0.5" value="27.5">
<div class="d-flex justify-content-between" style="font-size:.75rem;color:#6b7280"><span>20%</span><span>35%</span></div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Remise client (%)</label>
<input type="number" name="discount_pct" id="discount_pct" class="form-control" min="0" max="100" step="0.5" value="0">
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Notes internes</label>
<textarea name="notes" class="form-control" rows="2"></textarea>
</div>
</div>
</div>
<button type="submit" class="btn w-100 py-2 fw-bold" style="background:#ff6b35;color:#fff">
<i class="bi bi-save me-2"></i>Enregistrer la commande
</button>
</form>
</div>
<div class="col-lg-5">
<div class="card sticky-top" style="top:1.5rem">
<div class="card-header py-3"><i class="bi bi-receipt me-2"></i>Recapitulatif temps reel</div>
<div class="card-body" id="breakdown">
<div class="text-center text-muted py-4">
<i class="bi bi-calculator fs-2"></i>
<p class="mt-2 small">Remplissez le poids et la duree pour voir le calcul.</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function parse3mf() {
const file = document.getElementById('file3mf').files[0];
if (!file) { alert('Selectionnez un fichier .3mf'); return; }
const status = document.getElementById('parseStatus');
status.textContent = 'Analyse...';
const fd = new FormData();
fd.append('file', file);
fetch('/api/parse-3mf', {method:'POST', body:fd})
.then(r => r.json())
.then(d => {
let filled = [];
if (d.weight_g) { document.getElementById('weight_g').value = d.weight_g; filled.push('poids'); }
if (d.hours != null){ document.getElementById('hours').value = d.hours; filled.push('heures'); }
if (d.minutes != null){ document.getElementById('minutes').value = d.minutes; }
if (d.filename) {
document.getElementById('sourceFile').value = d.filename;
if (!document.querySelector('[name=name]').value)
document.querySelector('[name=name]').value = d.filename;
}
status.textContent = filled.length ? 'Extrait: '+filled.join(', ') : 'Donnees non trouvees';
status.style.color = filled.length ? '#22c55e' : '#f59e0b';
recalc();
})
.catch(() => { status.textContent = 'Erreur de lecture'; status.style.color='#ef4444'; });
}
document.getElementById('materialSelect').addEventListener('change', function() {
const opt = this.options[this.selectedIndex];
const stock = parseFloat(opt.dataset.stock)||0;
const threshold = parseFloat(opt.dataset.threshold)||200;
const info = document.getElementById('stockInfo');
if (!opt.value) { info.textContent=''; return; }
if (stock<=0) info.innerHTML='<span class="text-danger">RUPTURE DE STOCK</span>';
else if (stock<=threshold) info.innerHTML='<span class="text-warning">Stock bas: '+stock.toFixed(0)+' g</span>';
else info.innerHTML='<span class="text-success">Stock: '+stock.toFixed(0)+' g</span>';
recalc();
});
function setDM(val) {
document.getElementById('design_multiplier').value = val;
document.getElementById('dmLabel').textContent = 'x'+val.toFixed(2);
recalc();
}
document.getElementById('gross_margin_pct').addEventListener('input', function(){
document.getElementById('marginDisplay').textContent = this.value+' %'; recalc();
});
document.getElementById('design_multiplier').addEventListener('input', function(){
document.getElementById('dmLabel').textContent = 'x'+parseFloat(this.value).toFixed(2); recalc();
});
document.getElementById('clientSelect').addEventListener('change', function(){
const dm = this.options[this.selectedIndex].dataset.dm;
if (dm) setDM(parseFloat(dm));
});
['weight_g','hours','minutes','discount_pct'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('input', recalc);
});
function recalc() {
const w = parseFloat(document.getElementById('weight_g').value)||0;
const h = parseInt(document.getElementById('hours').value)||0;
const m = parseInt(document.getElementById('minutes').value)||0;
if (w<=0 || (h===0 && m===0)) return;
const body = {
weight_g: w, hours: h, minutes: m,
design_multiplier: parseFloat(document.getElementById('design_multiplier').value)||1.80,
gross_margin_pct: parseFloat(document.getElementById('gross_margin_pct').value)||27.5,
discount_pct: parseFloat(document.getElementById('discount_pct').value)||0,
material_id: document.getElementById('materialSelect').value||null,
};
fetch('/api/calculate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
.then(r=>r.json()).then(d=>renderBreakdown(d,body));
}
function fmt(n) { return parseFloat(n).toFixed(4)+' EUR'; }
function fmt2(n) { return parseFloat(n).toFixed(2)+' EUR'; }
function renderBreakdown(d, body) {
const dis = body.discount_pct>0
? '<div class="breakdown-row text-danger"><span>Remise ('+body.discount_pct+'%)</span></div>' : '';
document.getElementById('breakdown').innerHTML = `
<div class="breakdown-row"><span class="breakdown-muted">Matiere</span><span>${fmt(d.material_cost)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted">Marge matiere (20%)</span><span>${fmt(d.material_margin)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted">Design (x${body.design_multiplier.toFixed(2)})</span><span>${fmt(d.design_cost)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted">Manutention</span><span>${fmt(d.handling_cost)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted">Usure machine</span><span>${fmt(d.wear_cost)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted">Electricite</span><span>${fmt(d.electricity_cost)}</span></div>
<div class="breakdown-row subtotal"><span class="fw-semibold">Sous-total couts</span><span class="fw-semibold">${fmt2(d.subtotal)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted">Marge brute (${body.gross_margin_pct}%)</span><span>${fmt2(d.margin_amount)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted">Cotisations (12.3%)</span><span>${fmt2(d.tax_amount)}</span></div>
${dis}
<div class="breakdown-row total"><span>PRIX FINAL</span><span class="final-price">${fmt2(d.final_price)}</span></div>`;
}
</script>
{% endblock %}