fix: improve UI layout and add loading indicator for events

This commit is contained in:
2025-08-19 18:48:56 +02:00
parent 3efa7101e1
commit cec54a45d7

View File

@@ -10,9 +10,11 @@
</head> </head>
<body> <body>
<div id="connectedUser" class="position-fixed top-0 end-0 m-2 p-2 bg-light border rounded" style="display: none; z-index: 1000; font-size: 0.9rem;"> <div id="connectedUser" class="position-fixed top-0 end-0 m-2 p-2 bg-light border rounded"
style="display: none; z-index: 1000; font-size: 0.9rem;">
<span>Connecté: <span id="userName"></span></span> <span>Connecté: <span id="userName"></span></span>
<button id="disconnect" class="btn btn-danger btn-sm ms-2" style="font-size: 0.8rem; padding: 0.1rem 0.3rem;">Déco</button> <button id="disconnect" class="btn btn-danger btn-sm ms-2"
style="font-size: 0.8rem; padding: 0.1rem 0.3rem;">Déco</button>
</div> </div>
<div class="container-fluid d-flex align-items-center justify-content-center"> <div class="container-fluid d-flex align-items-center justify-content-center">
@@ -21,7 +23,7 @@
<div id="loginContainer" class="mb-3"> <div id="loginContainer" class="mb-3">
<div id="oidcLoginSection"> <div id="oidcLoginSection">
<button id="oidcLoginBtn" class="btn btn-primary btn-lg px-4 py-3 shadow">Se connecter avec Infomaniak</button> <button id="oidcLoginBtn" class="btn btn-primary btn-md px-4 py-3 shadow">Se connecter</button>
</div> </div>
</div> </div>
</div> </div>
@@ -30,25 +32,26 @@
<div class="container mt-2" id="mainContent" style="display: none;"> <div class="container mt-2" id="mainContent" style="display: none;">
<div id="eventFilters" style="display: none;"> <div id="eventFilters" style="display: none;">
<div class="row g-2 align-items-end"> <div class="mb-3">
<div class="col-auto"> <label for="account" class="form-label">Compte</label>
<label for="account" class="form-label mb-1">Compte</label> <select id="account" class="form-select">
<select id="account" class="form-select form-select-sm"> <option value="default">Défaut</option>
<option value="default">Défaut</option> </select>
</select> <label for="agegroup" class="form-label">Âge</label>
</div> <select id="agegroup" class="form-select">
<div class="col-auto"> <option value="">Tous</option>
<label for="agegroup" class="form-label mb-1">Âge</label> </select>
<select id="agegroup" class="form-select form-select-sm"> <button id="fetchEvents" class="btn btn-primary" style="margin-top: 1.2rem;">Charger</button>
<option value="">Tous</option>
</select>
</div>
<div class="col-auto">
<button id="fetchEvents" class="btn btn-primary btn-sm" style="margin-top: 1.2rem;">Charger</button>
</div>
</div> </div>
</div> </div>
<div id="loadingIndicator" class="text-center my-3" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
<p class="mt-2">Chargement des événements...</p>
</div>
<div id="eventList" class="row mt-2"></div> <div id="eventList" class="row mt-2"></div>
<!-- Modal pour afficher les détails d'un événement --> <!-- Modal pour afficher les détails d'un événement -->
@@ -66,7 +69,7 @@
</div> </div>
</div> </div>
<script> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const loginContainer = document.getElementById("loginContainer"); const loginContainer = document.getElementById("loginContainer");
const oidcLoginSection = document.getElementById("oidcLoginSection"); const oidcLoginSection = document.getElementById("oidcLoginSection");
@@ -89,17 +92,17 @@
fetch(`${apiBaseUrl}/userinfo`, { fetch(`${apiBaseUrl}/userinfo`, {
headers: { "Authorization": `Bearer ${storedApiKey}` } headers: { "Authorization": `Bearer ${storedApiKey}` }
}) })
.then(response => response.json()) .then(response => response.json())
.then(userData => { .then(userData => {
userInfo = {email: userData.email || "Utilisateur connecté"}; userInfo = { email: userData.email || "Utilisateur connecté" };
localStorage.setItem("userInfo", JSON.stringify(userInfo)); localStorage.setItem("userInfo", JSON.stringify(userInfo));
renderLoginSection(); renderLoginSection();
}) })
.catch(e => { .catch(e => {
console.error("Error fetching user info:", e); console.error("Error fetching user info:", e);
// If we can't fetch user info, proceed with rendering // If we can't fetch user info, proceed with rendering
renderLoginSection(); renderLoginSection();
}); });
} else { } else {
// Render the login section normally // Render the login section normally
renderLoginSection(); renderLoginSection();
@@ -136,49 +139,49 @@
fetch(`${apiBaseUrl}/userinfo`, { fetch(`${apiBaseUrl}/userinfo`, {
headers: { "Authorization": `Bearer ${accessToken}` } headers: { "Authorization": `Bearer ${accessToken}` }
}) })
.then(response => response.json()) .then(response => response.json())
.then(userData => { .then(userData => {
userInfo = {email: userData.email || "Utilisateur connecté"}; userInfo = { email: userData.email || "Utilisateur connecté" };
localStorage.setItem("userInfo", JSON.stringify(userInfo)); localStorage.setItem("userInfo", JSON.stringify(userInfo));
// Update UI // Update UI
renderLoginSection(); renderLoginSection();
}) })
.catch(e => { .catch(e => {
console.error("Error fetching user info:", e); console.error("Error fetching user info:", e);
// Fallback to parsing token as JWT // Fallback to parsing token as JWT
try { try {
// First, try to parse as JWT // First, try to parse as JWT
const tokenParts = accessToken.split('.'); const tokenParts = accessToken.split('.');
let userData = {}; let userData = {};
if (tokenParts.length === 3) { if (tokenParts.length === 3) {
// Standard JWT format // Standard JWT format
const payload = tokenParts[1]; const payload = tokenParts[1];
if (payload) { if (payload) {
// Add padding if needed // Add padding if needed
const paddedPayload = payload.padEnd(payload.length + (4 - payload.length % 4) % 4, '='); const paddedPayload = payload.padEnd(payload.length + (4 - payload.length % 4) % 4, '=');
const decodedPayload = atob(paddedPayload); const decodedPayload = atob(paddedPayload);
userData = JSON.parse(decodedPayload); userData = JSON.parse(decodedPayload);
}
} else {
// Non-JWT token, treat as opaque token
console.log("Non-JWT token received, using default user info");
userData = { email: "utilisateur@" + window.location.hostname };
} }
} else {
// Non-JWT token, treat as opaque token userInfo = { email: userData.email || "Utilisateur connecté" };
console.log("Non-JWT token received, using default user info"); localStorage.setItem("userInfo", JSON.stringify(userInfo));
userData = {email: "utilisateur@" + window.location.hostname}; } catch (parseError) {
console.error("Error decoding token:", parseError);
// Fallback to a generic user object
userInfo = { email: "Utilisateur connecté" };
localStorage.setItem("userInfo", JSON.stringify(userInfo));
} }
userInfo = {email: userData.email || "Utilisateur connecté"}; // Update UI
localStorage.setItem("userInfo", JSON.stringify(userInfo)); renderLoginSection();
} catch (parseError) { });
console.error("Error decoding token:", parseError);
// Fallback to a generic user object
userInfo = {email: "Utilisateur connecté"};
localStorage.setItem("userInfo", JSON.stringify(userInfo));
}
// Update UI
renderLoginSection();
});
// Remove token from URL // Remove token from URL
window.history.replaceState({}, document.title, "/"); window.history.replaceState({}, document.title, "/");
@@ -257,58 +260,58 @@
fetch(`${apiBaseUrl}/accounts`, { fetch(`${apiBaseUrl}/accounts`, {
headers: { "Authorization": `Bearer ${storedApiKey}` } headers: { "Authorization": `Bearer ${storedApiKey}` }
}) })
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
return response.json().then(errorData => { return response.json().then(errorData => {
console.error("Accounts error response:", errorData); console.error("Accounts error response:", errorData);
throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.detail || 'Unknown error'}`); throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.detail || 'Unknown error'}`);
});
}
return response.json();
})
.then(accounts => {
// Check if accounts is actually an array
if (!Array.isArray(accounts)) {
console.error("Accounts data is not an array:", accounts);
throw new Error("Invalid accounts data format");
}
accountSelect.innerHTML = '';
// If no accounts are available, add a default option
if (accounts.length === 0) {
const option = document.createElement("option");
option.value = "default";
option.textContent = "Default";
accountSelect.appendChild(option);
return;
}
// Add all available accounts
accounts.forEach(account => {
const option = document.createElement("option");
option.value = account.name;
option.textContent = account.label;
accountSelect.appendChild(option);
}); });
}
return response.json();
})
.then(accounts => {
// Check if accounts is actually an array
if (!Array.isArray(accounts)) {
console.error("Accounts data is not an array:", accounts);
throw new Error("Invalid accounts data format");
}
accountSelect.innerHTML = ''; // Select the stored account if it exists, otherwise select the first account
let accountToSelect = storedAccount;
if (!accounts.some(account => account.name === storedAccount)) {
accountToSelect = accounts[0].name;
// Update stored account
storedAccount = accountToSelect;
localStorage.setItem("account", accountToSelect);
}
// If no accounts are available, add a default option // Set the selected account in the dropdown
if (accounts.length === 0) { accountSelect.value = accountToSelect;
const option = document.createElement("option"); })
option.value = "default"; .catch(error => {
option.textContent = "Default"; console.error("Erreur lors du chargement des comptes:", error);
accountSelect.appendChild(option); alert("Accès refusé. Vous n'êtes pas autorisé à accéder à cette application. Veuillez contacter l'administrateur.");
return; logout();
}
// Add all available accounts
accounts.forEach(account => {
const option = document.createElement("option");
option.value = account.name;
option.textContent = account.label;
accountSelect.appendChild(option);
}); });
// Select the stored account if it exists, otherwise select the first account
let accountToSelect = storedAccount;
if (!accounts.some(account => account.name === storedAccount)) {
accountToSelect = accounts[0].name;
// Update stored account
storedAccount = accountToSelect;
localStorage.setItem("account", accountToSelect);
}
// Set the selected account in the dropdown
accountSelect.value = accountToSelect;
})
.catch(error => {
console.error("Erreur lors du chargement des comptes:", error);
alert("Accès refusé. Vous n'êtes pas autorisé à accéder à cette application. Veuillez contacter l'administrateur.");
logout();
});
} }
renderLoginSection(); renderLoginSection();
@@ -335,6 +338,12 @@
}); });
function fetchEvents(apiKey, account) { function fetchEvents(apiKey, account) {
// Show loading indicator
const loadingIndicator = document.getElementById("loadingIndicator");
const eventList = document.getElementById("eventList");
loadingIndicator.style.display = "block";
eventList.innerHTML = ""; // Clear previous events
fetch(`${apiBaseUrl}/schedule?account=${account}`, { fetch(`${apiBaseUrl}/schedule?account=${account}`, {
headers: { "Authorization": `Bearer ${apiKey}` } headers: { "Authorization": `Bearer ${apiKey}` }
}) })
@@ -370,6 +379,9 @@
}); });
}) })
.then(data => { .then(data => {
// Hide loading indicator
loadingIndicator.style.display = "none";
if (data) { if (data) {
// Check if data is an array // Check if data is an array
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
@@ -383,6 +395,9 @@
} }
}) })
.catch(error => { .catch(error => {
// Hide loading indicator on error
loadingIndicator.style.display = "none";
console.error("Erreur lors du chargement des événements:", error); console.error("Erreur lors du chargement des événements:", error);
alert("Erreur lors du chargement des événements: " + error.message); alert("Erreur lors du chargement des événements: " + error.message);
}); });