gmao/app_new/wizard/templates/wizard/equipment_wizard.html
root b1dd90e226
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Améliorer la création guidée des équipements
2026-08-16 23:08:02 +00:00

41 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Inventaire guidé — GMAO{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-start mb-3">
<div><h1 class="h3"><i class="bi bi-magic"></i> Inventaire guidé</h1><p class="text-muted mb-0">Sélectionnez les pièces, ajoutez plusieurs familles, vérifiez puis créez tout en une fois.</p></div>
<a href="{{ url_for('equipments.create', advanced=1) }}" class="btn btn-outline-secondary btn-sm">Fiche unitaire avancée</a>
</div>
<div class="card mb-3"><div class="card-header fw-semibold">1. Où se trouvent les équipements ?</div><div class="card-body">
<div class="row g-2 mb-3"><div class="col-md-3"><select id="building" class="form-select"><option value="">Tous les bâtiments</option>{% for b in buildings %}<option value="{{ b.id }}">{{ b.name }}</option>{% endfor %}</select></div><div class="col-md-3"><select id="zone" class="form-select"><option value="">Toutes les zones</option></select></div><div class="col-md-3"><select id="room-type" class="form-select"><option value="">Tous les types de salle</option>{% for t in room_types %}<option value="{{ t.id }}">{{ t.name }}</option>{% endfor %}</select></div><div class="col-md-3"><input id="room-search" class="form-control" placeholder="Rechercher une pièce"></div></div>
<div id="rooms" class="d-flex flex-wrap gap-2">{% for building in buildings %}{% for room in building.rooms|sort(attribute='name') %}<button type="button" class="btn btn-outline-secondary btn-sm room" data-id="{{ room.id }}" data-building="{{ building.id }}" data-zone="{{ room.zone_id or '' }}" data-type="{{ room.room_type_id or '' }}" data-search="{{ (building.name ~ ' ' ~ room.name ~ ' ' ~ (room.zone.name if room.zone else ''))|lower }}">{{ building.name }} · {{ room.name }}{% if room.room_type %} <small>({{ room.room_type.name }})</small>{% endif %}</button>{% endfor %}{% endfor %}</div>
<div class="mt-3"><strong id="room-count">0 pièce sélectionnée</strong> <button type="button" id="select-visible" class="btn btn-link btn-sm">Tout sélectionner dans le filtre</button> <button type="button" id="clear-rooms" class="btn btn-link btn-sm text-danger">Effacer</button></div>
</div></div>
<div class="card mb-3"><div class="card-header d-flex justify-content-between align-items-center"><span class="fw-semibold">2. Quoi inventorier ?</span><div><button type="button" class="btn btn-outline-success btn-sm preset" data-preset="classe">Salle de classe standard</button> <button type="button" class="btn btn-outline-success btn-sm preset" data-preset="logement">Logement standard</button> <button type="button" id="add-row" class="btn btn-primary btn-sm"><i class="bi bi-plus"></i> Ajouter une ligne</button></div></div>
<div class="card-body"><div class="alert alert-info py-2">Quantitatif : chaises ou prises comptées en nombre. Individualisé : chaque luminaire, fenêtre ou appareil possède sa propre fiche et son historique.</div>
<div class="table-responsive"><table class="table align-middle"><thead><tr><th>Nom</th><th>Catégorie / lot de maintenance</th><th>Qté par pièce</th><th>Suivi</th><th>Mobilité</th><th></th></tr></thead><tbody id="items"></tbody></table></div>
</div></div>
<div class="card mb-3"><div class="card-header fw-semibold">3. Vérification et aperçu</div><div class="card-body"><div id="summary">Sélectionnez une pièce et ajoutez un équipement.</div><div id="tree-preview" class="mt-3"></div></div></div>
<div class="d-flex justify-content-between"><a href="{{ url_for('equipments.index') }}" class="btn btn-outline-secondary">Annuler</a><button id="submit" class="btn btn-success btn-lg" disabled><i class="bi bi-check-lg"></i> Créer linventaire</button></div>
<template id="row-template"><tr class="item"><td><input class="form-control name" placeholder="Ex. Luminaire" required></td><td><select class="form-select category"><option value="">Catégorie…</option>{% for c in categories %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}</select><select class="form-select lot mt-1" disabled><option value="">Lot…</option></select><button type="button" class="btn btn-link btn-sm p-0 mt-1 quick-lot" disabled><i class="bi bi-plus-lg"></i> Nouveau lot dans cette catégorie</button></td><td><input type="number" min="1" max="1000" value="1" class="form-control quantity" style="min-width:6rem"></td><td><select class="form-select mode"><option value="quantitatif">Quantitatif</option><option value="individuel">Individualisé</option></select></td><td><select class="form-select mobility"><option value="non_precise">À préciser</option><option value="mobile">Mobile</option><option value="fixe">Fixe</option></select></td><td><button type="button" class="btn btn-outline-danger remove" title="Supprimer"><i class="bi bi-trash"></i></button></td></tr></template>
{% endblock %}
{% block extra_scripts %}<script>
const lots={{ lots_by_category|tojson }}, selected=new Set({{ selected_room_ids|tojson }}), items=document.getElementById('items');
const norm=s=>(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase();
function refreshRooms(){const b=document.getElementById('building').value,z=document.getElementById('zone').value,t=document.getElementById('room-type').value,q=norm(document.getElementById('room-search').value);document.querySelectorAll('.room').forEach(x=>{x.hidden=!!((b&&x.dataset.building!==b)||(z&&x.dataset.zone!==z)||(t&&x.dataset.type!==t)||(q&&!x.dataset.search.includes(q)));x.classList.toggle('btn-success',selected.has(+x.dataset.id));x.classList.toggle('btn-outline-secondary',!selected.has(+x.dataset.id));});document.getElementById('room-count').textContent=selected.size+' pièce'+(selected.size>1?'s':'')+' sélectionnée'+(selected.size>1?'s':'');refreshSummary()}
async function zones(){const s=document.getElementById('zone'),b=document.getElementById('building').value;s.innerHTML='<option value="">Toutes les zones</option>';if(b){for(const z of await (await fetch('/wizard/api/zones/'+b)).json())s.add(new Option(z.name,z.id))}refreshRooms()}
document.getElementById('rooms').onclick=e=>{const x=e.target.closest('.room');if(!x)return;const id=+x.dataset.id;selected.has(id)?selected.delete(id):selected.add(id);refreshRooms()};
document.getElementById('building').onchange=zones;document.getElementById('zone').onchange=refreshRooms;document.getElementById('room-type').onchange=refreshRooms;document.getElementById('room-search').oninput=refreshRooms;
document.getElementById('select-visible').onclick=()=>{document.querySelectorAll('.room:not([hidden])').forEach(x=>selected.add(+x.dataset.id));refreshRooms()};document.getElementById('clear-rooms').onclick=()=>{selected.clear();refreshRooms()};
function addRow(data={}){const row=document.getElementById('row-template').content.firstElementChild.cloneNode(true);items.append(row);row.querySelector('.name').value=data.name||'';const cat=row.querySelector('.category'),lot=row.querySelector('.lot'),quick=row.querySelector('.quick-lot');if(data.category){const option=[...cat.options].find(o=>norm(o.text).includes(norm(data.category)));if(option)cat.value=option.value}function fill(selected=''){lot.innerHTML='<option value="">Lot…</option>';for(const x of (lots[cat.value]||[]))lot.add(new Option(x.name,x.id));lot.disabled=!cat.value;quick.disabled=!cat.value;if(selected)lot.value=String(selected);else if(data.lot&&lot.options.length>1)lot.selectedIndex=1;refreshSummary()}cat.onchange=()=>fill();quick.onclick=async()=>{const categoryName=cat.selectedOptions[0]?.textContent.trim(),name=window.prompt(`Nom du nouveau lot pour « ${categoryName} » :`);if(!name||!name.trim())return;quick.disabled=true;const response=await fetch('{{ url_for("wizard.quick_create_lot") }}',{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':'{{ csrf_token() }}'},body:JSON.stringify({name:name.trim(),category_id:cat.value})}),result=await response.json();quick.disabled=false;if(!response.ok&&!result.lot){alert(result.error||'Création du lot impossible');return}const created=result.lot;lots[cat.value]=lots[cat.value]||[];if(!lots[cat.value].some(x=>String(x.id)===String(created.id)))lots[cat.value].push(created);fill(created.id);if(!response.ok)alert(result.error)};fill();row.querySelector('.quantity').value=data.quantity||1;row.querySelector('.mode').value=data.mode||'quantitatif';row.querySelector('.mobility').value=data.mobility||'non_precise';row.querySelectorAll('input,select').forEach(x=>x.addEventListener('input',refreshSummary));row.querySelector('.remove').onclick=()=>{row.remove();refreshSummary()};refreshSummary()}
document.getElementById('add-row').onclick=()=>addRow();
const presets={classe:[['Chaises','chaise',30,'quantitatif','mobile'],['Prises','prise',10,'individuel','fixe'],['Luminaires','luminaire',10,'individuel','fixe'],['Portes','porte',2,'individuel','fixe'],['Murs','mur',4,'quantitatif','fixe']],logement:[['Prises','prise',8,'individuel','fixe'],['Luminaires','luminaire',2,'individuel','fixe'],['Portes','porte',1,'individuel','fixe'],['Fenêtres','fenetre',1,'individuel','fixe']]};
document.querySelectorAll('.preset').forEach(b=>b.onclick=()=>presets[b.dataset.preset].forEach(x=>addRow({name:x[0],category:x[1],quantity:x[2],mode:x[3],mobility:x[4],lot:true})));
function payload(){return [...document.querySelectorAll('.item')].map(r=>({name:r.querySelector('.name').value.trim(),category_id:r.querySelector('.category').value,lot_id:r.querySelector('.lot').value,quantity:+r.querySelector('.quantity').value,mode:r.querySelector('.mode').value,mobility:r.querySelector('.mobility').value}))}
function refreshSummary(){const data=payload(),valid=selected.size&&data.length&&data.every(x=>x.name&&x.category_id&&x.lot_id&&x.quantity>0),units=data.reduce((n,x)=>n+x.quantity*selected.size,0);document.getElementById('summary').textContent=data.length?`${data.length} famille(s), ${selected.size} pièce(s), ${units} unité(s) au total. Les lots et maintenances seront hérités par chaque équipement.`:'Ajoutez au moins une ligne déquipement.';const tree={};document.querySelectorAll('.room').forEach(x=>{if(selected.has(+x.dataset.id)){const parts=x.textContent.trim().split(' · ');(tree[parts[0]]??=[]).push(parts[1])}});document.getElementById('tree-preview').innerHTML=Object.entries(tree).map(([b,rooms])=>`<div><i class="bi bi-building"></i> <strong>${b}</strong><ul>${rooms.map(r=>`<li><i class="bi bi-door-open"></i> ${r}</li>`).join('')}</ul></div>`).join('')||'<span class="text-muted">Aucun emplacement sélectionné.</span>';document.getElementById('submit').disabled=!valid}
document.getElementById('submit').onclick=async()=>{const b=document.getElementById('submit');b.disabled=true;const r=await fetch('{{ url_for("wizard.bulk_create_equipments") }}',{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':'{{ csrf_token() }}'},body:JSON.stringify({room_ids:[...selected],items:payload()})});const d=await r.json();if(!r.ok){alert(d.error||'Création impossible');refreshSummary();return}location.href='{{ url_for("equipments.index") }}'};
addRow();refreshRooms();
</script>{% endblock %}