gmao/app_new/templates/setup_wizard/index.html

218 lines
16 KiB
HTML
Raw Normal View History

2026-08-14 18:02:25 +02:00
{% 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>
2026-08-14 18:02:25 +02:00
<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 %}
2026-08-14 18:02:25 +02:00
</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>
2026-08-14 18:02:25 +02:00
</div>
</div>
2026-08-14 18:02:25 +02:00
</div>
<script>
const totalSteps = 10;
let displayStep = Math.min(Math.max(Number({{ progress.step|default(0) }}) + 1, 1), totalSteps);
const setupData = {{ progress.data|tojson }};
2026-08-14 18:02:25 +02:00
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(); }
2026-08-14 18:02:25 +02:00
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();
2026-08-14 18:02:25 +02:00
}
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;
2026-08-14 18:02:25 +02:00
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});
2026-08-14 18:02:25 +02:00
} 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);
2026-08-14 18:02:25 +02:00
} 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;
2026-08-14 18:02:25 +02:00
} else if (displayStep === 4) {
result = await saveStep(4, {zones: zones.filter(z => z.name?.trim())}); zones = result.zones;
2026-08-14 18:02:25 +02:00
} else if (displayStep === 5) {
result = await saveStep(5, {rooms: rooms.filter(r => r.name?.trim())}); rooms = result.rooms;
2026-08-14 18:02:25 +02:00
} else if (displayStep === 6) {
result = await saveStep(6, {});
2026-08-14 18:02:25 +02:00
} 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});
2026-08-14 18:02:25 +02:00
} else {
result = await saveStep(displayStep, {skipped: displayStep === 8 || displayStep === 9});
2026-08-14 18:02:25 +02:00
}
showStep(Math.min(displayStep + 1, totalSteps));
} catch (error) { alert(error.message); }
2026-08-14 18:02:25 +02:00
}
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); }
2026-08-14 18:02:25 +02:00
}
renderBuildings(); renderZones(); renderRooms(); showStep(displayStep);
2026-08-14 18:02:25 +02:00
</script>
{% endblock %}