feat: Appliquer le pattern optimistic update sur les pages admin

Chaque action inline (block, unblock, renvoi d'invitation, modification
de période, fin de remplacement, ajout de journée pédagogique) déclenchait
un rechargement complet de la liste via loadXxx(). Cela causait un reset
du scroll, un temps d'attente visible et des requêtes réseau inutiles.

Le pattern de mise à jour locale introduit dans la story Droit à l'image
est généralisé aux 4 pages admin restantes, en extrayant explicitement
les champs utiles de la réponse API pour éviter la pollution JSON-LD.
This commit is contained in:
2026-02-19 19:02:41 +01:00
parent 1b8bd6cd78
commit cfbe96ccf8
4 changed files with 87 additions and 7 deletions

View File

@@ -189,7 +189,29 @@
); );
} }
await loadPeriods(); const updated = await response.json();
if (config) {
const updatedPeriod: Period = {
sequence: editingPeriod!.sequence,
label: updated.label ?? editingPeriod!.label,
startDate: updated.startDate ?? editStartDate,
endDate: updated.endDate ?? editEndDate,
isCurrent: updated.isCurrent ?? editingPeriod!.isCurrent,
daysRemaining: updated.daysRemaining ?? editingPeriod!.daysRemaining,
isPast: updated.isPast ?? editingPeriod!.isPast
};
config = {
...config,
periods: config.periods.map((p) =>
p.sequence === editingPeriod!.sequence ? updatedPeriod : p
),
currentPeriod: updatedPeriod.isCurrent
? updatedPeriod
: config.currentPeriod?.sequence === editingPeriod!.sequence
? null
: config.currentPeriod
};
}
closeEditModal(); closeEditModal();
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : 'Erreur inconnue'; error = e instanceof Error ? e.message : 'Erreur inconnue';

View File

@@ -190,7 +190,27 @@
); );
} }
await loadCalendar(); const data = await response.json();
const newEntry: CalendarEntry = {
id: data.id ?? data['@id'] ?? '',
type: data.type ?? 'pedagogical',
startDate: data.startDate ?? data.date ?? pedDate,
endDate: data.endDate ?? data.date ?? pedDate,
label: data.label ?? pedLabel,
description: data.description ?? null
};
if (calendar) {
const entries = [...calendar.entries, newEntry].sort((a, b) =>
a.startDate.localeCompare(b.startDate)
);
calendar = { ...calendar, entries };
} else {
calendar = {
academicYearId,
zone: null,
entries: [newEntry]
};
}
showPedagogicalModal = false; showPedagogicalModal = false;
pedDate = ''; pedDate = '';
pedLabel = ''; pedLabel = '';

View File

@@ -330,9 +330,15 @@
throw new Error(message); throw new Error(message);
} }
replacements = replacements.filter((r) => r.id !== replacementToEnd!.id);
totalItems = Math.max(0, totalItems - 1);
if (replacements.length === 0 && currentPage > 1) {
currentPage -= 1;
updateUrl();
reloadReplacements();
}
successMessage = 'Remplacement terminé avec succès'; successMessage = 'Remplacement terminé avec succès';
closeEndModal(); closeEndModal();
await reloadReplacements();
globalThis.setTimeout(() => { globalThis.setTimeout(() => {
successMessage = null; successMessage = null;
}, 3000); }, 3000);

View File

@@ -246,8 +246,17 @@
throw new Error(errorMessage); throw new Error(errorMessage);
} }
const updated = await response.json().catch(() => null);
users = users.map((u) =>
u.id === userId
? {
...u,
invitedAt: updated?.invitedAt ?? new Date().toISOString(),
invitationExpiree: false
}
: u
);
successMessage = 'L\'invitation a été renvoyée avec succès.'; successMessage = 'L\'invitation a été renvoyée avec succès.';
await loadUsers();
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : 'Erreur lors du renvoi'; error = e instanceof Error ? e.message : 'Erreur lors du renvoi';
} finally { } finally {
@@ -361,9 +370,19 @@
throw new Error(errorMessage); throw new Error(errorMessage);
} }
const updated = await response.json();
users = users.map((u) =>
u.id === blockTargetUser!.id
? {
...u,
statut: updated.statut ?? 'suspended',
blockedAt: updated.blockedAt ?? new Date().toISOString(),
blockedReason: updated.blockedReason ?? blockReason.trim()
}
: u
);
successMessage = `L'utilisateur ${blockTargetUser.firstName} ${blockTargetUser.lastName} a été bloqué.`; successMessage = `L'utilisateur ${blockTargetUser.firstName} ${blockTargetUser.lastName} a été bloqué.`;
closeBlockModal(); closeBlockModal();
await loadUsers();
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : 'Erreur lors du blocage'; error = e instanceof Error ? e.message : 'Erreur lors du blocage';
} finally { } finally {
@@ -401,8 +420,18 @@
throw new Error(errorMessage); throw new Error(errorMessage);
} }
const updated = await response.json();
users = users.map((u) =>
u.id === user.id
? {
...u,
statut: updated.statut ?? 'active',
blockedAt: null,
blockedReason: null
}
: u
);
successMessage = `L'utilisateur ${user.firstName} ${user.lastName} a été débloqué.`; successMessage = `L'utilisateur ${user.firstName} ${user.lastName} a été débloqué.`;
await loadUsers();
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : 'Erreur lors du déblocage'; error = e instanceof Error ? e.message : 'Erreur lors du déblocage';
} finally { } finally {
@@ -440,9 +469,12 @@
isSavingRoles = true; isSavingRoles = true;
error = null; error = null;
await updateUserRoles(rolesTargetUser.id, selectedRoles); await updateUserRoles(rolesTargetUser.id, selectedRoles);
const updatedRoles = [...selectedRoles];
users = users.map((u) =>
u.id === rolesTargetUser!.id ? { ...u, roles: updatedRoles } : u
);
successMessage = `Les rôles de ${rolesTargetUser.firstName} ${rolesTargetUser.lastName} ont été mis à jour.`; successMessage = `Les rôles de ${rolesTargetUser.firstName} ${rolesTargetUser.lastName} ont été mis à jour.`;
closeRolesModal(); closeRolesModal();
await loadUsers();
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : 'Erreur lors de la mise à jour des rôles'; error = e instanceof Error ? e.message : 'Erreur lors de la mise à jour des rôles';
} finally { } finally {