Barre horizontale en haut de l'app qui liste les sites autorises de l'utilisateur et permet de switcher d'un click. Consomme le composant MalioSiteSelector de @malio/layer-ui 1.4.0 (upgrade depuis 1.3.0). Composables : - useModules (shared) : consomme /api/modules, expose isModuleActive. Pattern aligne sur useSidebar. - useCurrentSite (layer sites) : singleton state, switchSite optimistic avec rollback sur erreur, garde anti-double-submit, propagation au store auth via action setCurrentSite dediee. Composant : - SiteSelector.vue : wrapper thin autour de MalioSiteSelector. Texte blanc uniforme (conforme maquette Figma) avec taille 24px forcee via labelClass="text-2xl". aria-label du group via ariaGroupLabel i18n. Integration : - Middleware auth.global.ts : chargement parallele sidebar + modules. - layouts/default.vue : render conditionnel si module Sites actif ET user.sites.length > 0. - logout.vue : reset des 3 composables (sidebar, modules, currentSite) dans un try/finally. - nuxt.config.ts : auto-detection des composables/ de chaque layer module (necessaire car imports.dirs explicite override les defaults Nuxt). Couleurs fixtures finales : Chatellerault #056CF2, Saint-Jean #F3CB00, Pommevic #74BF04. Charge aux admins de choisir des teintes foncees (texte blanc non contrastable via calcul WCAG, design choisi). Tests : 40 Vitest (color, useModules, useSidebar, useCurrentSite, SiteSelector) incluant garde anti-regression pour useI18n hors setup. 182/182 PHPUnit backend, avec et sans module actif. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
/**
|
|
* Composable de gestion du site courant (ticket 3 module Sites).
|
|
*
|
|
* Pattern aligne sur `useSidebar` : state singleton au niveau module,
|
|
* hydrate depuis `useAuthStore().user`, mute de maniere optimistic avec
|
|
* rollback si la requete PATCH `/api/me/current-site` echoue.
|
|
*
|
|
* Garantie d'unicite : le flag `switching` bloque les double-clicks
|
|
* concurrents. Le reset explicite est appele au logout
|
|
* (voir `modules/core/pages/logout.vue`).
|
|
*
|
|
* Auto-select : aucun. Le backend (`UserRbacProcessor::ensureCurrentSiteConsistency`)
|
|
* garantit deja l'invariant "user avec sites non vide => currentSite non null"
|
|
* apres tout PATCH /rbac. Le front consomme l'etat renvoye tel quel.
|
|
*
|
|
* Contrainte d'appel : `useCurrentSite()` doit etre invoque au top du
|
|
* `setup()` d'un composant (ou d'un autre composable appele au setup).
|
|
* Les dependances `useI18n`, `useApi` et `useAuthStore` sont resolues
|
|
* a l'initialisation et reutilisees par `switchSite` — ceci evite le
|
|
* "Must be called at the top of a setup function" qui se produirait
|
|
* si on les appelait paresseusement depuis une fonction async declenchee
|
|
* par un handler d'event (hors contexte setup).
|
|
*/
|
|
import { ref } from 'vue'
|
|
import type { Site } from '~/shared/types/sites'
|
|
|
|
const currentSite = ref<Site | null>(null)
|
|
const availableSites = ref<Site[]>([])
|
|
const switching = ref(false)
|
|
|
|
export function useCurrentSite() {
|
|
// Resolution au setup : les 3 services doivent etre invoques dans un
|
|
// contexte composant. Leur capture ici permet a switchSite() de
|
|
// s'executer plus tard (handler de click, async) sans crash.
|
|
const auth = useAuthStore()
|
|
const api = useApi()
|
|
const { t } = useI18n()
|
|
|
|
/**
|
|
* Synchronise le state singleton depuis le store auth. A appeler au
|
|
* mount du SiteSelector (ou via un watcher sur `auth.user`).
|
|
*/
|
|
function syncFromAuth(): void {
|
|
availableSites.value = auth.user?.sites ?? []
|
|
currentSite.value = auth.user?.currentSite ?? null
|
|
}
|
|
|
|
/**
|
|
* Bascule le site courant. Optimistic UI : la mutation locale precede
|
|
* la requete HTTP. En cas d'echec (`api.patch` throw), l'etat local est
|
|
* restaure — le store auth n'a PAS ete muté a ce stade (la propagation
|
|
* `auth.setCurrentSite` se fait uniquement apres un succes HTTP), donc
|
|
* aucun rollback cote auth n'est necessaire.
|
|
*
|
|
* Garde anti-double-submit : si un switch est deja en vol, le second
|
|
* appel est un no-op silencieux.
|
|
*/
|
|
async function switchSite(site: Site): Promise<void> {
|
|
if (switching.value) {
|
|
return
|
|
}
|
|
|
|
const previousLocal = currentSite.value
|
|
currentSite.value = site
|
|
switching.value = true
|
|
|
|
try {
|
|
await api.patch(
|
|
'/me/current-site',
|
|
{ site: `/api/sites/${site.id}` },
|
|
{ toastSuccessMessage: t('sites.selector.switchSuccess') },
|
|
)
|
|
// Propage au store auth via l'action dediee — plus tracable que
|
|
// la mutation directe et garantit la notification des watchers.
|
|
// N'est appele qu'apres un succes HTTP donc pas de rollback a
|
|
// prevoir sur cette ligne.
|
|
auth.setCurrentSite(site)
|
|
} catch (error) {
|
|
currentSite.value = previousLocal
|
|
throw error
|
|
} finally {
|
|
switching.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Vide l'etat singleton. Appele au logout pour eviter qu'un user
|
|
* suivant (connecte sur le meme onglet) voie les sites de l'ancien.
|
|
*/
|
|
function resetCurrentSite(): void {
|
|
currentSite.value = null
|
|
availableSites.value = []
|
|
switching.value = false
|
|
}
|
|
|
|
return {
|
|
currentSite,
|
|
availableSites,
|
|
switching,
|
|
switchSite,
|
|
syncFromAuth,
|
|
resetCurrentSite,
|
|
}
|
|
}
|