gmao/app_new/templates/setup_wizard/index.html
root a0e8d3003d
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Fiabilise l'import initial du patrimoine
2026-08-14 18:20:16 +00:00

217 lines
16 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 %}Configuration initiale — GMAO{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="card shadow mx-auto" style="max-width: 1200px">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="bi bi-gear-fill"></i> Configuration initiale GMAO</h4>
</div>
<div class="card-body">
<div class="d-flex flex-wrap justify-content-between gap-2 mb-4" id="stepIndicators">
{% set labels = ['Admin', 'Collège', 'Bâtiments', 'Zones', 'Salles', 'Lots', 'Calendrier', 'ENT', 'Pronote', 'Fin'] %}
{% for label in labels %}
<div class="text-center flex-fill small" id="step{{ loop.index }}-indicator">
<i class="bi bi-{{ loop.index }}-circle fs-4"></i><div>{{ label }}</div>
</div>
{% endfor %}
</div>
<div id="step1" class="step-panel">
<h5>Compte administrateur</h5>
<div class="mb-3"><label class="form-label" for="adminUsername">Nom dutilisateur</label>
<input class="form-control" id="adminUsername" minlength="3" value="{{ progress.data.admin_username }}" required>
</div>
<div class="mb-3"><label class="form-label" for="adminPassword">Mot de passe</label>
<input type="password" class="form-control" id="adminPassword" minlength="12" autocomplete="new-password" required>
<div class="form-text">12 caractères minimum.</div>
</div>
</div>
<div id="step2" class="step-panel d-none">
<h5>Établissement</h5>
<div class="mb-2"><label class="form-label" for="collegeName">Nom</label><input class="form-control" id="collegeName" value="{{ progress.data.college.name }}"></div>
<div class="mb-2"><label class="form-label" for="collegeAddress">Adresse</label><input class="form-control" id="collegeAddress" value="{{ progress.data.college.address }}"></div>
<div class="row"><div class="col-md-4 mb-2"><label class="form-label" for="collegeZip">Code postal</label><input class="form-control" id="collegeZip" value="{{ progress.data.college.zip }}"></div>
<div class="col-md-8 mb-2"><label class="form-label" for="collegeCity">Ville</label><input class="form-control" id="collegeCity" value="{{ progress.data.college.city }}"></div></div>
</div>
<div id="step3" class="step-panel d-none">
<h5>Bâtiments</h5>
<p class="text-muted">Les bâtiments existants sont chargés et mis à jour. Retirer une ligne de lécran ne supprime jamais le bâtiment ni ses locaux.</p>
<div id="buildingsList"></div>
<button class="btn btn-outline-primary btn-sm" type="button" onclick="addBuilding()"><i class="bi bi-plus-circle"></i> Ajouter</button>
</div>
<div id="step4" class="step-panel d-none">
<h5>Zones</h5>
<p class="text-muted">Chaque zone appartient à un bâtiment. Les zones déjà importées sont conservées.</p>
<div id="zonesList"></div>
<button class="btn btn-outline-primary btn-sm" type="button" onclick="addZone()"><i class="bi bi-plus-circle"></i> Ajouter</button>
</div>
<div id="step5" class="step-panel d-none">
<h5>Salles et locaux</h5>
<p class="text-muted">Une salle peut être affectée à une zone, avec son étage et son code. Les salles existantes ne sont jamais supprimées par le wizard.</p>
<div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>Nom</th><th>Code</th><th>Bâtiment</th><th>Zone</th><th>Étage</th><th></th></tr></thead><tbody id="roomsList"></tbody></table></div>
<button class="btn btn-outline-primary btn-sm" type="button" onclick="addRoom()"><i class="bi bi-plus-circle"></i> Ajouter</button>
</div>
<div id="step6" class="step-panel d-none">
<h5>Catalogue des lots et catégories</h5>
<div class="alert alert-info">Le catalogue issu du dump SQL est intégré à lapplication. Cette étape crée les entrées manquantes et corrige leurs associations sans supprimer les données existantes.</div>
<div class="row mb-3"><div class="col"><strong>{{ lots_data|length }}</strong> catégories</div><div class="col"><strong>{{ lots_data.values()|map('length')|sum }}</strong> lots</div></div>
<div class="accordion" id="catalogAccordion">
{% for category, lots in lots_data.items() %}
<div class="accordion-item"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#catalog{{ loop.index }}">{{ category }} <span class="badge bg-secondary ms-2">{{ lots|length }}</span></button></h2>
<div id="catalog{{ loop.index }}" class="accordion-collapse collapse" data-bs-parent="#catalogAccordion"><div class="accordion-body"><ul class="mb-0">{% for lot in lots %}<li>{{ lot }}</li>{% endfor %}</ul></div></div></div>
{% endfor %}
</div>
</div>
<div id="step7" class="step-panel d-none">
<h5>Calendrier</h5>
<div class="row"><div class="col-md-6"><label class="form-label" for="academiqueZone">Zone académique</label><select class="form-select" id="academiqueZone"><option value="A">Zone A</option><option value="B">Zone B</option><option value="C">Zone C</option></select></div>
<div class="col-md-6"><label class="form-label" for="anneeScolaire">Année de début</label><input type="number" class="form-control" id="anneeScolaire" min="2024" value="{{ progress.data.vacances_annee or 2026 }}"></div></div>
<div class="form-check mt-3"><input class="form-check-input" type="checkbox" id="importAutoVacances" checked><label class="form-check-label" for="importAutoVacances">Importer les vacances officielles lorsquun importeur sera configuré</label></div>
<div class="form-check"><input class="form-check-input" type="checkbox" id="importAutoFeries" checked><label class="form-check-label" for="importAutoFeries">Importer les jours fériés lorsquun importeur sera configuré</label></div>
</div>
<div id="step8" class="step-panel d-none"><h5>ENT</h5><div class="alert alert-secondary">Configuration facultative. Elle sera réalisée depuis la page dédiée après linstallation ; tant quelle est absente, son watchdog reste arrêté.</div></div>
<div id="step9" class="step-panel d-none"><h5>Pronote</h5><div class="alert alert-secondary">Configuration facultative. Elle sera réalisée depuis la page dédiée après linstallation ; tant quelle est absente, son watchdog reste arrêté.</div></div>
<div id="step10" class="step-panel d-none"><h5>Terminer</h5><div class="alert alert-success">Vérifiez les étapes précédentes, puis finalisez linstallation.</div></div>
<div class="d-flex justify-content-between mt-4">
<button id="prevBtn" class="btn btn-secondary" type="button" onclick="previousStep()"><i class="bi bi-arrow-left"></i> Précédent</button>
<button id="nextBtn" class="btn btn-primary" type="button" onclick="nextStep()">Suivant <i class="bi bi-arrow-right"></i></button>
<button id="finishBtn" class="btn btn-success d-none" type="button" onclick="finishSetup()"><i class="bi bi-check-lg"></i> Terminer</button>
</div>
</div>
</div>
</div>
<script>
const totalSteps = 10;
let displayStep = Math.min(Math.max(Number({{ progress.step|default(0) }}) + 1, 1), totalSteps);
const setupData = {{ progress.data|tojson }};
let buildings = setupData.buildings || [];
let zones = setupData.zones || [];
let rooms = setupData.rooms || [];
function field(value, placeholder, type = 'text') {
const input = document.createElement('input');
input.className = 'form-control form-control-sm'; input.type = type;
input.value = value ?? ''; input.placeholder = placeholder; return input;
}
function selectField(options, value, emptyLabel = null) {
const select = document.createElement('select'); select.className = 'form-select form-select-sm';
if (emptyLabel !== null) select.add(new Option(emptyLabel, ''));
options.forEach(item => select.add(new Option(item.label, String(item.value))));
select.value = value == null ? '' : String(value); return select;
}
function removeButton(callback) {
const button = document.createElement('button'); button.type = 'button'; button.className = 'btn btn-outline-secondary btn-sm';
button.title = 'Retirer de cette saisie sans supprimer en base'; button.innerHTML = '<i class="bi bi-x-lg"></i>'; button.onclick = callback; return button;
}
function renderBuildings() {
const root = document.getElementById('buildingsList'); root.replaceChildren();
buildings.forEach((item, index) => {
const row = document.createElement('div'); row.className = 'row g-2 mb-2';
const nameCol = document.createElement('div'); nameCol.className = 'col-md-5'; const name = field(item.name, 'Nom du bâtiment'); name.oninput = () => item.name = name.value; nameCol.append(name);
const descCol = document.createElement('div'); descCol.className = 'col-md-6'; const desc = field(item.description, 'Description'); desc.oninput = () => item.description = desc.value; descCol.append(desc);
const action = document.createElement('div'); action.className = 'col-md-1'; action.append(removeButton(() => { buildings.splice(index, 1); renderBuildings(); }));
row.append(nameCol, descCol, action); root.append(row);
});
}
function addBuilding() { buildings.push({name: '', description: ''}); renderBuildings(); }
function buildingOptions() { return buildings.filter(b => b.id && b.name).map(b => ({value: b.id, label: b.name})); }
function renderZones() {
const root = document.getElementById('zonesList'); root.replaceChildren();
zones.forEach((item, index) => {
const row = document.createElement('div'); row.className = 'row g-2 mb-2';
const nameCol = document.createElement('div'); nameCol.className = 'col-md-6'; const name = field(item.name, 'Nom de la zone'); name.oninput = () => item.name = name.value; nameCol.append(name);
const buildingCol = document.createElement('div'); buildingCol.className = 'col-md-5'; const building = selectField(buildingOptions(), item.building_id, 'Choisir un bâtiment'); building.onchange = () => item.building_id = Number(building.value); buildingCol.append(building);
const action = document.createElement('div'); action.className = 'col-md-1'; action.append(removeButton(() => { zones.splice(index, 1); renderZones(); }));
row.append(nameCol, buildingCol, action); root.append(row);
});
}
function addZone() { zones.push({name: '', building_id: buildings[0]?.id || null}); renderZones(); }
function zoneOptions(buildingId) { return zones.filter(z => Number(z.building_id) === Number(buildingId) && z.id).map(z => ({value: z.id, label: z.name})); }
function renderRooms() {
const root = document.getElementById('roomsList'); root.replaceChildren();
rooms.forEach((item, index) => {
const row = document.createElement('tr');
const nameCell = document.createElement('td'); const name = field(item.name, 'Nom'); name.oninput = () => item.name = name.value; nameCell.append(name);
const codeCell = document.createElement('td'); const code = field(item.code, 'Code'); code.oninput = () => item.code = code.value; codeCell.append(code);
const buildingCell = document.createElement('td'); const building = selectField(buildingOptions(), item.building_id); building.onchange = () => { item.building_id = Number(building.value); item.zone_id = null; renderRooms(); }; buildingCell.append(building);
const zoneCell = document.createElement('td'); const zone = selectField(zoneOptions(item.building_id), item.zone_id, 'Sans zone'); zone.onchange = () => item.zone_id = zone.value ? Number(zone.value) : null; zoneCell.append(zone);
const floorCell = document.createElement('td'); const floor = field(item.floor ?? 0, '0', 'number'); floor.style.width = '75px'; floor.oninput = () => item.floor = Number(floor.value); floorCell.append(floor);
const action = document.createElement('td'); action.append(removeButton(() => { rooms.splice(index, 1); renderRooms(); }));
row.append(nameCell, codeCell, buildingCell, zoneCell, floorCell, action); root.append(row);
});
}
function addRoom() { rooms.push({name: '', code: '', building_id: buildings[0]?.id || null, zone_id: null, floor: 0}); renderRooms(); }
function showStep(step) {
displayStep = step;
document.querySelectorAll('.step-panel').forEach((panel, index) => panel.classList.toggle('d-none', index + 1 !== step));
for (let i = 1; i <= totalSteps; i++) {
const icon = document.querySelector(`#step${i}-indicator i`);
icon.className = i < step ? 'bi bi-check-circle-fill fs-4 text-success' : `bi bi-${i}-circle fs-4 ${i === step ? 'text-primary' : 'text-secondary'}`;
}
document.getElementById('prevBtn').classList.toggle('invisible', step === 1);
document.getElementById('nextBtn').classList.toggle('d-none', step === totalSteps);
document.getElementById('finishBtn').classList.toggle('d-none', step !== totalSteps);
if (step === 3) renderBuildings(); if (step === 4) renderZones(); if (step === 5) renderRooms();
}
async function saveStep(step, data) {
const response = await fetch(`/setup-wizard/api/step/${step}`, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
const result = await response.json();
if (!response.ok || !result.success) throw new Error(result.error || 'Erreur de sauvegarde');
return result;
}
async function nextStep() {
try {
let result;
if (displayStep === 1) {
const username = document.getElementById('adminUsername').value.trim(), password = document.getElementById('adminPassword').value;
if (username.length < 3 || password.length < 12) throw new Error('Le nom doit faire 3 caractères et le mot de passe 12 caractères minimum.');
result = await saveStep(1, {username, password});
} else if (displayStep === 2) {
const data = {name: document.getElementById('collegeName').value.trim(), address: document.getElementById('collegeAddress').value.trim(), zip: document.getElementById('collegeZip').value.trim(), city: document.getElementById('collegeCity').value.trim()};
if (!data.name || !data.address || !/^\d{5}$/.test(data.zip) || !data.city) throw new Error('Complétez les informations du collège (code postal à 5 chiffres).');
result = await saveStep(2, data);
} else if (displayStep === 3) {
const valid = buildings.filter(b => b.name?.trim()); if (!valid.length) throw new Error('Ajoutez au moins un bâtiment.');
result = await saveStep(3, {buildings: valid}); buildings = result.buildings;
} else if (displayStep === 4) {
result = await saveStep(4, {zones: zones.filter(z => z.name?.trim())}); zones = result.zones;
} else if (displayStep === 5) {
result = await saveStep(5, {rooms: rooms.filter(r => r.name?.trim())}); rooms = result.rooms;
} else if (displayStep === 6) {
result = await saveStep(6, {});
} else if (displayStep === 7) {
result = await saveStep(7, {zone: document.getElementById('academiqueZone').value, annee: Number(document.getElementById('anneeScolaire').value), import_vacances: document.getElementById('importAutoVacances').checked, import_feries: document.getElementById('importAutoFeries').checked});
} else {
result = await saveStep(displayStep, {skipped: displayStep === 8 || displayStep === 9});
}
showStep(Math.min(displayStep + 1, totalSteps));
} catch (error) { alert(error.message); }
}
function previousStep() { if (displayStep > 1) showStep(displayStep - 1); }
async function finishSetup() {
try {
await saveStep(10, {});
const response = await fetch('/setup-wizard/api/complete', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{}'});
const result = await response.json(); if (!response.ok || !result.success) throw new Error(result.error || 'Finalisation impossible');
window.location.assign('/auth/login');
} catch (error) { alert(error.message); }
}
renderBuildings(); renderZones(); renderRooms(); showStep(displayStep);
</script>
{% endblock %}