Files
Jo e410bfbc16
Deploy via Portainer / deploy (push) Successful in 0s
feat: color picker palette + filtre/tri/duplicate matières
2026-07-06 23:47:39 +02:00

247 lines
11 KiB
HTML

{% extends 'base.html' %}
{% block title %}{% if material %}Modifier{% else %}Nouvelle{% endif %} matière{% endblock %}
{% block content %}
<div class="page-header">
<a href="{{ url_for('materials') }}" class="text-muted text-decoration-none small">
<i class="bi bi-arrow-left"></i> Matières
</a>
<h1 class="mt-1">{% if material %}Modifier — {{ material.name }}{% else %}Nouvelle matière{% endif %}</h1>
</div>
<div class="row g-4">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<form method="POST">
<div class="row g-3">
<div class="col-8">
<label class="form-label fw-semibold">Nom *</label>
<input type="text" name="name" class="form-control" required
value="{{ material.name if material else '' }}"
placeholder="ex: PLA+ Rouge Bambu">
</div>
<div class="col-4">
<label class="form-label fw-semibold">Type</label>
<select name="type" class="form-select">
{% for t in ['PLA','PLA+','PETG','ASA','ABS','TPU','Nylon','PC','Résine','Autre'] %}
<option value="{{ t }}" {% if material and material.type==t %}selected{% endif %}>{{ t }}</option>
{% endfor %}
</select>
</div>
<div class="col-6">
<label class="form-label fw-semibold">Marque</label>
<input type="text" name="brand" class="form-control"
value="{{ material.brand if material else '' }}"
placeholder="Bambu, eSUN, Polymaker…">
</div>
<div class="col-12">
<label class="form-label fw-semibold">Couleur</label>
<!-- Valeur soumise (hex) -->
<input type="hidden" name="color" id="colorValue"
value="{{ (material.color | css_color) if material and material.color else '' }}">
<!-- Swatches preset -->
<div id="swatchGrid" class="mb-2 d-flex flex-wrap gap-1">
<!-- Générés par JS -->
</div>
<!-- Ligne : preview + picker natif + hex -->
<div class="d-flex align-items-center gap-2">
<span id="colorPreview"
style="width:32px;height:32px;border-radius:6px;border:2px solid #dee2e6;
flex-shrink:0;background:{{ (material.color | css_color) if material and material.color else '#cccccc' }}"></span>
<input type="color" id="nativePicker" class="form-control form-control-color p-0 border-0"
style="width:36px;height:32px;cursor:pointer"
value="{{ (material.color | css_color) if material and material.color else '#cccccc' }}"
title="Choisir une couleur personnalisée">
<input type="text" id="hexInput" class="form-control form-control-sm font-monospace"
style="max-width:110px" placeholder="#rrggbb"
value="{{ (material.color | css_color) if material and material.color else '' }}">
<button type="button" class="btn btn-sm btn-outline-secondary" id="clearColor"
title="Aucune couleur">
<i class="bi bi-x"></i>
</button>
</div>
</div>
<div class="col-6">
<label class="form-label fw-semibold">Prix (€/kg) *</label>
<input type="number" name="price_per_kg" class="form-control"
step="0.01" min="0" required
value="{{ material.price_per_kg if material else '25.00' }}">
</div>
<div class="col-6">
<label class="form-label fw-semibold">Stock actuel (g)</label>
<input type="number" name="stock_g" class="form-control"
step="1" min="0"
value="{{ material.stock_g|round(0)|int if material else 0 }}">
<div class="form-text">1 rouleau ≈ 1000 g</div>
</div>
<div class="col-6">
<label class="form-label fw-semibold">Seuil alerte stock bas (g)</label>
<input type="number" name="low_stock_threshold_g" class="form-control"
step="50" min="0"
value="{{ material.low_stock_threshold_g|round(0)|int if material else 200 }}">
</div>
<div class="col-12">
<label class="form-label fw-semibold">Notes</label>
<textarea name="notes" class="form-control" rows="2">{{ material.notes if material else '' }}</textarea>
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn" style="background:#ff6b35;color:#fff">
<i class="bi bi-save me-1"></i>Enregistrer
</button>
<a href="{{ url_for('materials') }}" class="btn btn-outline-secondary">Annuler</a>
</div>
</form>
</div>
</div>
</div>
{% if history %}
<div class="col-md-4">
<div class="card">
<div class="card-header py-3"><i class="bi bi-clock-history me-2"></i>Historique des prix</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<thead class="table-light"><tr><th>Date</th><th class="text-end">€/kg</th></tr></thead>
<tbody>
{% for h in history %}
<tr>
<td class="text-muted small">{{ h.recorded_at[:10] }}</td>
<td class="text-end fw-semibold">{{ h.price_per_kg }} €</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
(function () {
// ── Palette de preset filaments ──────────────────────────────────────
const PRESETS = [
// Blancs / Neutres
{ hex: '#ffffff', label: 'Blanc' },
{ hex: '#f5f0e8', label: 'Naturel' },
{ hex: '#e8e0d0', label: 'Crème' },
{ hex: '#d4b896', label: 'Beige' },
{ hex: '#9ca3af', label: 'Argent' },
{ hex: '#6b7280', label: 'Gris' },
{ hex: '#374151', label: 'Gris foncé' },
{ hex: '#111111', label: 'Noir' },
// Rouges / Roses
{ hex: '#fca5a5', label: 'Rose clair' },
{ hex: '#f87171', label: 'Saumon' },
{ hex: '#ef4444', label: 'Rouge' },
{ hex: '#dc2626', label: 'Rouge foncé' },
{ hex: '#ec4899', label: 'Rose' },
{ hex: '#db2777', label: 'Fuchsia' },
// Oranges / Jaunes
{ hex: '#fb923c', label: 'Orange clair' },
{ hex: '#f97316', label: 'Orange' },
{ hex: '#ea580c', label: 'Orange foncé' },
{ hex: '#fbbf24', label: 'Jaune' },
{ hex: '#d97706', label: 'Or' },
{ hex: '#eab308', label: 'Jaune vif' },
// Verts
{ hex: '#86efac', label: 'Vert clair' },
{ hex: '#4ade80', label: 'Vert lime' },
{ hex: '#22c55e', label: 'Vert' },
{ hex: '#16a34a', label: 'Vert foncé' },
{ hex: '#15803d', label: 'Forêt' },
{ hex: '#65a30d', label: 'Olive' },
// Bleus / Cyans
{ hex: '#7dd3fc', label: 'Bleu ciel' },
{ hex: '#38bdf8', label: 'Ciel' },
{ hex: '#06b6d4', label: 'Cyan' },
{ hex: '#3b82f6', label: 'Bleu' },
{ hex: '#2563eb', label: 'Bleu vif' },
{ hex: '#1d4ed8', label: 'Bleu foncé' },
{ hex: '#1e3a8a', label: 'Marine' },
// Violets / Mauves
{ hex: '#c4b5fd', label: 'Lavande' },
{ hex: '#a78bfa', label: 'Mauve' },
{ hex: '#8b5cf6', label: 'Violet' },
{ hex: '#7c3aed', label: 'Violet foncé' },
{ hex: '#6366f1', label: 'Indigo' },
{ hex: '#d946ef', label: 'Magenta' },
// Marrons
{ hex: '#b45309', label: 'Marron clair' },
{ hex: '#92400e', label: 'Marron' },
{ hex: '#78350f', label: 'Marron foncé' },
// Spéciaux
{ hex: 'rgba(200,200,200,0.3)', label: 'Translucide', css: 'repeating-linear-gradient(45deg,#ccc 0,#ccc 3px,#fff 3px,#fff 8px)' },
];
const colorValue = document.getElementById('colorValue');
const colorPreview = document.getElementById('colorPreview');
const nativePicker = document.getElementById('nativePicker');
const hexInput = document.getElementById('hexInput');
const swatchGrid = document.getElementById('swatchGrid');
// ── Construire les swatches ──────────────────────────────────────────
PRESETS.forEach(p => {
const btn = document.createElement('button');
btn.type = 'button';
btn.title = p.label;
btn.dataset.hex = p.hex;
btn.style.cssText = [
'width:24px', 'height:24px', 'border-radius:5px', 'border:2px solid transparent',
`background:${p.css || p.hex}`, 'cursor:pointer', 'padding:0', 'flex-shrink:0',
'transition:transform .1s,border-color .1s'
].join(';');
btn.addEventListener('mouseenter', () => { btn.style.transform = 'scale(1.3)'; });
btn.addEventListener('mouseleave', () => { if (btn.dataset.hex !== colorValue.value) btn.style.transform = ''; });
btn.addEventListener('click', () => setColor(p.hex));
swatchGrid.appendChild(btn);
});
// ── Fonction centrale ────────────────────────────────────────────────
function setColor(hex) {
colorValue.value = hex;
hexInput.value = hex;
colorPreview.style.background = hex;
if (/^#[0-9a-fA-F]{6}$/.test(hex)) nativePicker.value = hex;
// Highlight swatch actif
swatchGrid.querySelectorAll('button').forEach(b => {
const active = b.dataset.hex === hex;
b.style.borderColor = active ? '#ff6b35' : 'transparent';
b.style.transform = active ? 'scale(1.3)' : '';
});
}
// ── Native color picker ──────────────────────────────────────────────
nativePicker.addEventListener('input', () => setColor(nativePicker.value));
// ── Saisie hex manuelle ──────────────────────────────────────────────
hexInput.addEventListener('input', function () {
const v = this.value.trim();
if (/^#[0-9a-fA-F]{6}$/.test(v)) setColor(v);
else { colorValue.value = v; colorPreview.style.background = v; }
});
// ── Effacer ──────────────────────────────────────────────────────────
document.getElementById('clearColor').addEventListener('click', () => {
colorValue.value = '';
hexInput.value = '';
colorPreview.style.background = '#e5e7eb';
swatchGrid.querySelectorAll('button').forEach(b => {
b.style.borderColor = 'transparent';
b.style.transform = '';
});
});
// ── Init ─────────────────────────────────────────────────────────────
const init = colorValue.value;
if (init) setColor(init);
})();
</script>
{% endblock %}