261 lines
11 KiB
HTML
261 lines
11 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}Commandes{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-header d-flex justify-content-between align-items-center">
|
|
<h1><i class="bi bi-printer me-2 text-primary"></i>Commandes</h1>
|
|
<div class="d-flex gap-2">
|
|
<a href="{{ url_for('export_csv') }}" class="btn btn-sm btn-outline-secondary">
|
|
<i class="bi bi-download me-1"></i>Export CSV
|
|
</a>
|
|
<a href="{{ url_for('new_job') }}" class="btn btn-sm" style="background:#ff6b35;color:#fff">
|
|
<i class="bi bi-plus-lg me-1"></i>Nouveau
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Barre de filtres -->
|
|
<div class="card mb-3">
|
|
<div class="card-body py-2 px-3">
|
|
<div class="row g-2 align-items-center">
|
|
<div class="col-auto d-flex align-items-center gap-1">
|
|
<label class="text-muted small mb-0">Du</label>
|
|
<input type="date" id="fDateFrom" class="form-control form-control-sm" style="width:140px">
|
|
</div>
|
|
<div class="col-auto d-flex align-items-center gap-1">
|
|
<label class="text-muted small mb-0">au</label>
|
|
<input type="date" id="fDateTo" class="form-control form-control-sm" style="width:140px">
|
|
</div>
|
|
<div class="col-auto">
|
|
<select id="fClient" class="form-select form-select-sm" style="min-width:160px">
|
|
<option value="">Tous les clients</option>
|
|
{% for c in clients %}
|
|
<option value="{{ c.id }}">{{ c.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-auto d-flex align-items-center gap-1">
|
|
<label class="text-muted small mb-0">Tri</label>
|
|
<select id="fSort" class="form-select form-select-sm" style="min-width:165px">
|
|
<option value="date-desc">Date (recent)</option>
|
|
<option value="date-asc">Date (ancien)</option>
|
|
<option value="prix-desc">Prix HT desc</option>
|
|
<option value="prix-asc">Prix HT asc</option>
|
|
<option value="poids-desc">Poids desc</option>
|
|
<option value="poids-asc">Poids asc</option>
|
|
<option value="duree-desc">Duree desc</option>
|
|
<option value="duree-asc">Duree asc</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-auto ms-auto">
|
|
<button id="fReset" class="btn btn-sm btn-outline-secondary">
|
|
<i class="bi bi-x-circle me-1"></i>Reinitialiser
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Resume -->
|
|
<div class="row g-2 mb-3">
|
|
<div class="col-auto">
|
|
<div class="card px-3 py-2">
|
|
<div class="text-muted" style="font-size:.68rem;text-transform:uppercase;letter-spacing:.05em">Commandes</div>
|
|
<div class="fw-bold fs-5" id="sumCount">{{ jobs|length }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<div class="card px-3 py-2">
|
|
<div class="text-muted" style="font-size:.68rem;text-transform:uppercase;letter-spacing:.05em">Total HT commandes</div>
|
|
<div class="fw-bold fs-5" id="sumHT">--</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<div class="card px-3 py-2">
|
|
<div class="text-muted" style="font-size:.68rem;text-transform:uppercase;letter-spacing:.05em">Total prix final</div>
|
|
<div class="fw-bold fs-5" id="sumFinal" style="color:#ff6b35">--</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<div class="card px-3 py-2">
|
|
<div class="text-muted" style="font-size:.68rem;text-transform:uppercase;letter-spacing:.05em">Poids total</div>
|
|
<div class="fw-bold fs-5" id="sumPoids">--</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tableau -->
|
|
<div class="card">
|
|
<div class="card-body p-0">
|
|
{% if jobs %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0" id="jobsTable">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width:90px">Date</th>
|
|
<th>Nom</th>
|
|
<th>Client</th>
|
|
<th>Matiere</th>
|
|
<th>Poids</th>
|
|
<th>Duree</th>
|
|
<th class="text-end">Prix HT</th>
|
|
<th class="text-end">Prix final</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="jobsBody">
|
|
{% for j in jobs %}
|
|
{% set pieces = j.pieces_per_plate or 1 %}
|
|
{% set qty = j.order_qty or 1 %}
|
|
{% set prix_ht = j.prix_ht_piece or 0 %}
|
|
{% set poids_piece = j.weight_g / pieces %}
|
|
<tr
|
|
data-date="{{ j.created_at[:10] }}"
|
|
data-client="{{ j.client_id or '' }}"
|
|
data-prix="{{ '%.4f' | format(prix_ht * qty) }}"
|
|
data-poids="{{ j.weight_g }}"
|
|
data-duree="{{ j.print_time_s }}"
|
|
data-final="{{ '%.2f' | format((j.final_price or 0) * qty) }}"
|
|
>
|
|
<td class="text-muted small align-middle">{{ j.created_at[:10] }}</td>
|
|
<td class="align-middle">
|
|
<a href="{{ url_for('job_detail', id=j.id) }}" class="text-decoration-none fw-semibold">{{ j.name }}</a>
|
|
{% if j.plate_name %}<br><span class="text-muted" style="font-size:.72rem">{{ j.plate_name }}</span>{% endif %}
|
|
</td>
|
|
<td class="align-middle small">{{ j.client_name or '--' }}</td>
|
|
<td class="align-middle small">
|
|
{% if j.material_name %}<span class="badge bg-light text-dark border">{{ j.material_name }}</span>{% else %}<span class="text-muted">--</span>{% endif %}
|
|
</td>
|
|
<td class="align-middle">
|
|
<span class="fw-semibold">{{ j.weight_g }} g</span>
|
|
{% if pieces > 1 %}
|
|
<br><span class="text-muted" style="font-size:.72rem">{{ '%.1f'|format(poids_piece) }} g/pce x{{ pieces }}</span>
|
|
{% else %}
|
|
<br><span class="text-muted" style="font-size:.72rem">{{ '%.1f'|format(poids_piece) }} g/pce</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="align-middle small">{{ j.print_time_s | fmt_time }}</td>
|
|
<td class="align-middle text-end">
|
|
{% if prix_ht > 0 %}
|
|
<span class="fw-semibold">{{ '%.2f'|format(prix_ht) }} €</span><span class="text-muted" style="font-size:.72rem"> /pce</span>
|
|
{% if qty > 1 %}
|
|
<br><span class="text-muted" style="font-size:.78rem">{{ '%.2f'|format(prix_ht * qty) }} € x{{ qty }}</span>
|
|
{% endif %}
|
|
{% else %}--{% endif %}
|
|
</td>
|
|
<td class="align-middle text-end">
|
|
<span class="fw-bold" style="color:#ff6b35">{{ '%.2f'|format(j.final_price or 0) }} €</span>
|
|
{% if j.discount_pct and j.discount_pct > 0 %}
|
|
<span class="badge bg-danger ms-1" style="font-size:.65rem">-{{ j.discount_pct }}%</span>
|
|
{% endif %}
|
|
{% if qty > 1 %}
|
|
<br><span class="text-muted" style="font-size:.72rem">{{ '%.2f'|format((j.final_price or 0) * qty) }} € tot.</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="align-middle">
|
|
<div class="d-flex gap-1 align-items-center">
|
|
{% if j.client_token %}
|
|
<a href="/portal/{{ j.client_token }}" target="_blank"
|
|
class="btn btn-sm btn-outline-success py-0" title="Portail client">
|
|
<i class="bi bi-share-fill"></i>
|
|
</a>
|
|
{% endif %}
|
|
<a href="{{ url_for('job_detail', id=j.id) }}" class="btn btn-sm btn-outline-secondary py-0">→</a>
|
|
<form method="POST" action="{{ url_for('delete_job', id=j.id) }}"
|
|
onsubmit="return confirm('Supprimer ?')">
|
|
<button class="btn btn-sm btn-outline-danger py-0"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div id="noResults" class="text-center text-muted py-4 d-none">
|
|
<i class="bi bi-search fs-2"></i>
|
|
<p class="mt-2 mb-0">Aucune commande ne correspond aux filtres.</p>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center text-muted py-5">
|
|
<i class="bi bi-inbox fs-1"></i>
|
|
<p class="mt-2">Aucune commande enregistree.</p>
|
|
<a href="{{ url_for('new_job') }}" class="btn btn-sm" style="background:#ff6b35;color:#fff">Creer le premier calcul</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
var body = document.getElementById('jobsBody');
|
|
if (!body) return;
|
|
var rows = Array.from(body.querySelectorAll('tr'));
|
|
var fFrom = document.getElementById('fDateFrom');
|
|
var fTo = document.getElementById('fDateTo');
|
|
var fCli = document.getElementById('fClient');
|
|
var fSort = document.getElementById('fSort');
|
|
var noRes = document.getElementById('noResults');
|
|
|
|
function fmt(v) {
|
|
return new Intl.NumberFormat('fr-FR', {minimumFractionDigits:2, maximumFractionDigits:2}).format(v) + ' €';
|
|
}
|
|
function fmtG(v) {
|
|
return new Intl.NumberFormat('fr-FR', {maximumFractionDigits:0}).format(v) + ' g';
|
|
}
|
|
|
|
function apply() {
|
|
var from = fFrom.value;
|
|
var to = fTo.value;
|
|
var client = fCli.value;
|
|
var parts = fSort.value.split('-');
|
|
var key = parts[0];
|
|
var dir = parts[1];
|
|
|
|
var visible = rows.filter(function(r) {
|
|
if (from && r.dataset.date < from) return false;
|
|
if (to && r.dataset.date > to) return false;
|
|
if (client && r.dataset.client !== client) return false;
|
|
return true;
|
|
});
|
|
|
|
visible.sort(function(a, b) {
|
|
var va, vb;
|
|
if (key === 'date') { va = a.dataset.date; vb = b.dataset.date; }
|
|
if (key === 'prix') { va = parseFloat(a.dataset.prix); vb = parseFloat(b.dataset.prix); }
|
|
if (key === 'poids') { va = parseFloat(a.dataset.poids); vb = parseFloat(b.dataset.poids); }
|
|
if (key === 'duree') { va = parseFloat(a.dataset.duree); vb = parseFloat(b.dataset.duree); }
|
|
if (dir === 'asc') return va > vb ? 1 : va < vb ? -1 : 0;
|
|
return va < vb ? 1 : va > vb ? -1 : 0;
|
|
});
|
|
|
|
rows.forEach(function(r) { r.style.display = 'none'; });
|
|
visible.forEach(function(r) { r.style.display = ''; body.appendChild(r); });
|
|
noRes.classList.toggle('d-none', visible.length > 0);
|
|
|
|
var totalHT = 0, totalFinal = 0, totalPoids = 0;
|
|
visible.forEach(function(r) {
|
|
totalHT += parseFloat(r.dataset.prix) || 0;
|
|
totalFinal += parseFloat(r.dataset.final) || 0;
|
|
totalPoids += parseFloat(r.dataset.poids) || 0;
|
|
});
|
|
document.getElementById('sumCount').textContent = visible.length;
|
|
document.getElementById('sumHT').textContent = fmt(totalHT);
|
|
document.getElementById('sumFinal').textContent = fmt(totalFinal);
|
|
document.getElementById('sumPoids').textContent = fmtG(totalPoids);
|
|
}
|
|
|
|
fFrom.addEventListener('change', apply);
|
|
fTo.addEventListener('change', apply);
|
|
fCli.addEventListener('change', apply);
|
|
fSort.addEventListener('change', apply);
|
|
document.getElementById('fReset').addEventListener('click', function() {
|
|
fFrom.value = ''; fTo.value = ''; fCli.value = ''; fSort.value = 'date-desc';
|
|
apply();
|
|
});
|
|
|
|
apply();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|