Files
Coltura/frontend/modules/sites/composables/useCurrentSite.ts
tristan 6cf5ef4cfc
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s
Module sites (#8)
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [x] Pas de régression
- [x] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [ ] CHANGELOG modifié

Co-authored-by: Matthieu <mtholot19@gmail.com>
Reviewed-on: #8
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-04-20 15:31:58 +00:00

131 lines
4.9 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'
import { onAuthSessionCleared } from '~/shared/stores/auth'
const currentSite = ref<Site | null>(null)
const availableSites = ref<Site[]>([])
const switching = ref(false)
// Enregistrement unique au niveau module (singleton) : quand clearSession()
// est appelee par l'intercepteur 401 de useApi, le state local est purgé
// de la meme facon qu'au logout explicite (logout.vue).
onAuthSessionCleared(() => {
currentSite.value = null
availableSites.value = []
switching.value = 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()
const { loadSidebar } = useSidebar()
/**
* 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)
// Apres un switch reussi : recharger la sidebar (les filtres de
// modules peuvent dependre du site courant via SiteScopedQueryExtension)
// et invalider toutes les donnees de page pour eviter que l'utilisateur
// voie les donnees de l'ancien site sous un toast "Site change".
try {
await loadSidebar()
} catch {
// No-op : la sidebar non rafraichie n'est pas bloquante.
}
try {
await refreshNuxtData()
} catch {
// No-op : certaines pages n'ont pas de useAsyncData a invalider.
}
} 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,
}
}