Améliorer la création guidée des équipements
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
This commit is contained in:
parent
801a0c856d
commit
b1dd90e226
2 changed files with 14 additions and 8 deletions
|
|
@ -2,7 +2,11 @@
|
|||
{% block title %}{{ title }} — GMAO Collège{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="mb-4">{{ title }}</h1>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div><h1 class="mb-1">{{ title }}</h1><p class="text-muted mb-0">Fiche détaillée d’un équipement unique ou d’un groupe d’équipements.</p></div>
|
||||
<a href="{{ url_for('wizard.equipment_wizard') }}" class="btn btn-outline-primary"><i class="bi bi-magic"></i> Création guidée en série</a>
|
||||
</div>
|
||||
<div class="alert alert-info"><i class="bi bi-info-circle"></i> Utilisez cette fiche pour un équipement précis (luminaire, prise, fenêtre…). Pour créer le même équipement dans plusieurs salles, utilisez la création guidée.</div>
|
||||
<div class="card"><div class="card-body">
|
||||
<form method="POST" id="equipmentForm">
|
||||
{{ form.hidden_tag() }}
|
||||
|
|
@ -17,6 +21,7 @@
|
|||
<div class="col-md-4 mb-3">{{ form.parent_id.label(class="form-label") }}{{ form.parent_id(class="form-select") }}</div>
|
||||
</div>
|
||||
|
||||
<h5 class="border-bottom pb-2">Organisation et quantité</h5>
|
||||
{# Gestion des groupes d'équipements #}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
|
|
@ -52,6 +57,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h5 class="border-bottom pb-2 mt-4">Dates et description</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">{{ form.install_date.label(class="form-label") }}{{ form.install_date(class="form-control", type="date") }}</div>
|
||||
<div class="col-md-6 mb-3">{{ form.warranty_end.label(class="form-label") }}{{ form.warranty_end(class="form-control", type="date") }}</div>
|
||||
|
|
@ -83,4 +89,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
toggleGroupOptions(); // Initial state
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
</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-4"><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-4"><select id="zone" class="form-select"><option value="">Toutes les zones</option></select></div><div class="col-md-4"><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-search="{{ (building.name ~ ' ' ~ room.name ~ ' ' ~ (room.zone.name if room.zone else ''))|lower }}">{{ building.name }} · {{ room.name }}</button>{% endfor %}{% endfor %}</div>
|
||||
<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>
|
||||
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
<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</div><div class="card-body" id="summary">Sélectionnez une pièce et ajoutez un équipement.</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 l’inventaire</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>
|
||||
|
|
@ -25,17 +25,17 @@
|
|||
{% 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,q=norm(document.getElementById('room-search').value);document.querySelectorAll('.room').forEach(x=>{x.hidden=!!((b&&x.dataset.building!==b)||(z&&x.dataset.zone!==z)||(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()}
|
||||
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-search').oninput=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.';document.getElementById('submit').disabled=!valid}
|
||||
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 %}
|
||||
|
|
|
|||
Loading…
Reference in a new issue