363 lines
No EOL
18 KiB
HTML
363 lines
No EOL
18 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Wizard Création Équipements — GMAO Collège{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<h1><i class="bi bi-magic"></i> Création d'équipements par lot</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-8">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-1-circle"></i> Étape 1 : Catégorie et Lot</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-md-12 mb-3">
|
|
<label class="form-label">Nom de l'équipement</label>
|
|
<input type="text" id="equipment_name" class="form-control" placeholder="Laisser vide pour utiliser le nom de la catégorie">
|
|
<small class="text-muted">Optionnel. Si vide, le nom de la catégorie sera utilisé.</small>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-3">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Catégorie *</label>
|
|
<select id="category_id" class="form-select">
|
|
<option value="">-- Sélectionner une catégorie --</option>
|
|
{% for cat in categories %}
|
|
<option value="{{ cat.id }}">{{ cat.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<small class="text-muted">Type d'équipement (Fenêtre, Porte, etc.)</small>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Lot *</label>
|
|
<select id="lot_id" class="form-select" disabled>
|
|
<option value="">-- Sélectionner d'abord une catégorie --</option>
|
|
</select>
|
|
<small class="text-muted" id="lot_hint">
|
|
<i class="bi bi-info-circle"></i> Sélectionnez d'abord une catégorie
|
|
</small>
|
|
|
|
<div id="lot_tasks_container" class="mt-3" style="display: none;">
|
|
<h6><i class="bi bi-clipboard-check"></i> Maintenances préventives du lot</h6>
|
|
<div id="lot_tasks_list" class="table-responsive"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-2-circle"></i> Étape 2 : Groupe parent (optionnel)</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="form-check mb-3">
|
|
<input type="checkbox" class="form-check-input" id="create_parent">
|
|
<label class="form-check-label" for="create_parent">Créer un groupe parent global</label>
|
|
</div>
|
|
<div id="parent_options" style="display: none;">
|
|
<div class="mb-3">
|
|
<label class="form-label">Nom du groupe parent</label>
|
|
<input type="text" id="parent_name" class="form-control" placeholder="Ex: Fenêtres du collège">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-3-circle"></i> Étape 3 : Salles et quantités</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">Bâtiment</label>
|
|
<select id="building_id" 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">
|
|
<label class="form-label">Zone (optionnel)</label>
|
|
<select id="zone_id" class="form-select">
|
|
<option value="">-- Toutes les zones --</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Type de salle</label>
|
|
<select id="room_type_id" class="form-select">
|
|
<option value="">-- Tous les types --</option>
|
|
{% for rt in room_types %}
|
|
<option value="{{ rt.id }}">{{ rt.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Suivi individuel</label>
|
|
<div class="form-check">
|
|
<input type="checkbox" class="form-check-input" id="tracked_individually">
|
|
<label class="form-check-label" for="tracked_individually">
|
|
Créer des équipements individuels (chaque fenêtre sera suivie séparément)
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<h6>Salles sélectionnées</h6>
|
|
<div id="rooms_list" class="list-group mb-3">
|
|
<div class="list-group-item text-muted">
|
|
<i class="bi bi-info-circle"></i> Sélectionnez des salles dans le panneau de droite
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-4-circle"></i> Étape 4 : Prévisualisation</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<button type="button" id="btn_preview" class="btn btn-outline-primary mb-3">
|
|
<i class="bi bi-eye"></i> Prévisualiser les équipements
|
|
</button>
|
|
<div id="preview_result" style="display: none;">
|
|
<table class="table table-sm">
|
|
<thead><tr><th>Équipement</th><th>Salle</th><th>Catégorie</th><th>Lot</th></tr></thead>
|
|
<tbody id="preview_body"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<a href="{{ url_for('equipments.index') }}" class="btn btn-secondary">
|
|
<i class="bi bi-x-lg"></i> Annuler
|
|
</a>
|
|
<button type="button" id="btn_submit" class="btn btn-primary" disabled>
|
|
<i class="bi bi-check-lg"></i> Créer les équipements
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-door-open"></i> Salles disponibles</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<input type="text" id="room_search" class="form-control mb-3" placeholder="Rechercher une salle...">
|
|
<div id="rooms_by_building">
|
|
{% for building in buildings %}
|
|
<div class="building-group" data-building-id="{{ building.id }}">
|
|
<h6>{{ building.name }}</h6>
|
|
<div class="room-list">
|
|
{% for room in building.rooms|sort(attribute='name') %}
|
|
<button type="button" class="btn btn-outline-secondary btn-sm mb-1 me-1 room-item"
|
|
data-room-id="{{ room.id }}" data-room-name="{{ room.name }}"
|
|
data-building-id="{{ building.id }}"
|
|
data-zone-id="{{ room.zone_id if room.zone_id else '' }}"
|
|
data-zone-name="{{ room.zone.name if room.zone else '' }}"
|
|
data-room-type-id="{{ room.room_type_id if room.room_type_id else '' }}">
|
|
{{ room.name }}
|
|
{% if room.zone %}<small class="text-muted">({{ room.zone.name }})</small>{% endif %}
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<select id="all_lots" style="display: none;">
|
|
{% for lot in lots %}
|
|
<option value="{{ lot.id }}">{{ lot.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
let selectedRooms = [];
|
|
let lotsByCategory = {{ lots_by_category | tojson | safe }};
|
|
|
|
document.getElementById('create_parent').addEventListener('change', function() {
|
|
document.getElementById('parent_options').style.display = this.checked ? 'block' : 'none';
|
|
});
|
|
|
|
document.getElementById('category_id').addEventListener('change', function() {
|
|
const categoryId = this.value;
|
|
const lotSelect = document.getElementById('lot_id');
|
|
lotSelect.innerHTML = '<option value="">-- Sélectionner un lot --</option>';
|
|
|
|
if (!categoryId) { lotSelect.disabled = true; return; }
|
|
|
|
const lots = lotsByCategory[categoryId] || [];
|
|
if (lots.length === 0) {
|
|
const allLots = document.getElementById('all_lots').querySelectorAll('option');
|
|
allLots.forEach(opt => { if (opt.value) lotSelect.appendChild(opt.cloneNode(true)); });
|
|
} else {
|
|
lots.forEach(lot => { lotSelect.appendChild(new Option(lot.name, lot.id)); });
|
|
}
|
|
lotSelect.disabled = false;
|
|
});
|
|
|
|
document.getElementById('lot_id').addEventListener('change', async function() {
|
|
const lotId = this.value;
|
|
const container = document.getElementById('lot_tasks_container');
|
|
const list = document.getElementById('lot_tasks_list');
|
|
|
|
if (!lotId) { container.style.display = 'none'; return; }
|
|
|
|
try {
|
|
const response = await fetch('/wizard/api/lot_tasks/' + lotId);
|
|
const tasks = await response.json();
|
|
|
|
if (tasks.length === 0) {
|
|
container.style.display = 'block';
|
|
list.innerHTML = '<p class="text-muted small">Aucune maintenance préventive définie pour ce lot.</p>';
|
|
return;
|
|
}
|
|
|
|
let html = '<table class="table table-sm table-hover"><thead><tr>' +
|
|
'<th>N°</th><th>Tâche</th><th>Périodicité</th><th>Contrat</th><th>Gestionnaire</th></tr></thead><tbody>';
|
|
tasks.forEach(task => {
|
|
html += '<tr><td>' + (task.num_tache || '-') + '</td><td>' + (task.tache || '-') + '</td>' +
|
|
'<td>' + (task.periodicite || '-') + '</td>' +
|
|
'<td>' + (task.contrat ? '<i class="bi bi-check text-success"></i>' : '-') + '</td>' +
|
|
'<td>' + (task.gestionnaire || '-') + '</td></tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
list.innerHTML = html;
|
|
container.style.display = 'block';
|
|
} catch (e) { container.style.display = 'none'; }
|
|
});
|
|
|
|
function filterRooms() {
|
|
const buildingId = document.getElementById('building_id').value;
|
|
const zoneId = document.getElementById('zone_id').value;
|
|
const roomTypeId = document.getElementById('room_type_id').value;
|
|
document.querySelectorAll('.room-item').forEach(item => {
|
|
const show = (!buildingId || item.dataset.buildingId === buildingId) &&
|
|
(!zoneId || item.dataset.zoneId === zoneId) &&
|
|
(!roomTypeId || item.dataset.roomTypeId === roomTypeId);
|
|
item.style.display = show ? '' : 'none';
|
|
});
|
|
document.querySelectorAll('.building-group').forEach(g => {
|
|
const gid = g.dataset.buildingId;
|
|
g.style.display = (buildingId && gid !== buildingId) ? 'none' : '';
|
|
});
|
|
}
|
|
|
|
document.getElementById('building_id').addEventListener('change', async function() {
|
|
const buildingId = this.value;
|
|
const zoneSelect = document.getElementById('zone_id');
|
|
zoneSelect.innerHTML = '<option value="">-- Toutes les zones --</option>';
|
|
if (buildingId) {
|
|
const zones = await (await fetch('/wizard/api/zones/' + buildingId)).json();
|
|
zones.forEach(z => zoneSelect.appendChild(new Option(z.name, z.id)));
|
|
}
|
|
filterRooms();
|
|
});
|
|
|
|
document.getElementById('zone_id').addEventListener('change', filterRooms);
|
|
document.getElementById('room_type_id').addEventListener('change', filterRooms);
|
|
|
|
document.getElementById('room_search').addEventListener('input', function() {
|
|
const search = this.value.toLowerCase();
|
|
document.querySelectorAll('.room-item').forEach(item => {
|
|
item.style.display = item.dataset.roomName.toLowerCase().includes(search) ? '' : 'none';
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.room-item').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const roomId = this.dataset.roomId;
|
|
const roomName = this.dataset.roomName;
|
|
const zoneName = this.dataset.zoneName || '';
|
|
const idx = selectedRooms.findIndex(r => r.id == roomId);
|
|
if (idx > -1) {
|
|
selectedRooms.splice(idx, 1);
|
|
this.classList.replace('btn-primary', 'btn-outline-secondary');
|
|
} else {
|
|
selectedRooms.push({id: roomId, name: roomName, zone: zoneName, count: 1});
|
|
this.classList.replace('btn-outline-secondary', 'btn-primary');
|
|
}
|
|
updateRoomsList();
|
|
});
|
|
});
|
|
|
|
function updateRoomsList() {
|
|
const list = document.getElementById('rooms_list');
|
|
if (selectedRooms.length === 0) {
|
|
list.innerHTML = '<div class="list-group-item text-muted"><i class="bi bi-info-circle"></i> Sélectionnez des salles</div>';
|
|
document.getElementById('btn_submit').disabled = true;
|
|
return;
|
|
}
|
|
list.innerHTML = selectedRooms.map(r => '<div class="list-group-item d-flex justify-content-between">' +
|
|
'<span>' + r.name + (r.zone ? ' <small class="text-muted">(' + r.zone + ')</small>' : '') + '</span>' +
|
|
'<input type="number" class="form-control form-control-sm room-count" style="width:80px" data-room-id="' + r.id + '" value="' + (r.count || 1) + '" min="1">' +
|
|
'</div>').join('');
|
|
document.querySelectorAll('.room-count').forEach(i => i.addEventListener('change', function() {
|
|
const room = selectedRooms.find(r => r.id == this.dataset.roomId);
|
|
if (room) room.count = parseInt(this.value) || 1;
|
|
}));
|
|
document.getElementById('btn_submit').disabled = false;
|
|
}
|
|
|
|
document.getElementById('btn_preview').addEventListener('click', function() {
|
|
const cat = document.getElementById('category_id');
|
|
const lot = document.getElementById('lot_id');
|
|
const nameInput = document.getElementById('equipment_name');
|
|
if (!cat.value || !lot.value || selectedRooms.length === 0) {
|
|
alert('Sélectionnez une catégorie, un lot et des salles.'); return;
|
|
}
|
|
const equipmentName = nameInput.value.trim() || cat.options[cat.selectedIndex].text;
|
|
const catName = cat.options[cat.selectedIndex].text;
|
|
const lotName = lot.options[lot.selectedIndex].text;
|
|
document.getElementById('preview_body').innerHTML = selectedRooms.flatMap(r => {
|
|
const count = r.count || 1;
|
|
return Array.from({length: count}, (_, i) => '<tr><td>' + equipmentName + (count > 1 ? ' - ' + (i+1) : '') + '</td><td>' + r.name + '</td><td>' + catName + '</td><td>' + lotName + '</td></tr>');
|
|
}).join('');
|
|
document.getElementById('preview_result').style.display = 'block';
|
|
});
|
|
|
|
document.getElementById('btn_submit').addEventListener('click', async function() {
|
|
const data = {
|
|
category_id: document.getElementById('category_id').value,
|
|
lot_id: document.getElementById('lot_id').value,
|
|
name: document.getElementById('equipment_name').value.trim(),
|
|
create_parent: document.getElementById('create_parent').checked,
|
|
parent_name: document.getElementById('parent_name').value,
|
|
building_id: document.getElementById('building_id').value,
|
|
zone_id: document.getElementById('zone_id').value,
|
|
rooms: selectedRooms.map(r => ({room_id: r.id, count: r.count || 1})),
|
|
tracked_individually: document.getElementById('tracked_individually').checked
|
|
};
|
|
|
|
try {
|
|
const resp = await fetch('/wizard/equipments/create', {
|
|
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)
|
|
});
|
|
const result = await resp.json();
|
|
if (result.success) {
|
|
alert(result.count + ' équipement(s) créé(s) avec succès !');
|
|
window.location.href = '/equipments/';
|
|
}
|
|
else alert('Erreur: ' + (result.error || 'Erreur inconnue'));
|
|
} catch (e) { alert('Erreur lors de la création.'); }
|
|
});
|
|
</script>
|
|
{% endblock %} |