diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ToutesLesFichesAction/FichesActionListe.tsx b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ToutesLesFichesAction/FichesActionListe.tsx index a598e1c0c6..2b46f54651 100644 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ToutesLesFichesAction/FichesActionListe.tsx +++ b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ToutesLesFichesAction/FichesActionListe.tsx @@ -27,6 +27,7 @@ import { FicheResume } from 'packages/api/src/plan-actions'; import ActionsGroupeesMenu from '../ActionsGroupees/ActionsGroupeesMenu'; import EmptyFichePicto from '../FicheAction/FichesLiees/EmptyFichePicto'; import { useCreateFicheAction } from '../FicheAction/data/useCreateFicheAction'; +import { useFicheActionCount } from '../FicheAction/data/useFicheActionCount'; import { useCreatePlanAction } from '../PlanAction/data/useUpsertAxe'; type sortByOptionsType = SortFichesAction & { @@ -140,7 +141,7 @@ const FichesActionListe = ({ const { data, isLoading } = useFicheResumesFetch({ options: ficheResumesOptions, }); - const hasFiches = !!data?.data?.length; + const { count: hasFiches } = useFicheActionCount(); /** Gère les fiches sélectionnées pour les actions groupées */ const handleSelectFiche = (fiche: FicheResume) => { diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/Trajectoire/ComparezLaTrajectoire.tsx b/app.territoiresentransitions.react/src/app/pages/collectivite/Trajectoire/ComparezLaTrajectoire.tsx index 8bb5aec09f..199473efc9 100644 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/Trajectoire/ComparezLaTrajectoire.tsx +++ b/app.territoiresentransitions.react/src/app/pages/collectivite/Trajectoire/ComparezLaTrajectoire.tsx @@ -1,24 +1,42 @@ import { makeCollectiviteIndicateursUrl } from '@/app/app/paths'; import { Button, Card } from '@/ui'; +interface ComparezLaTrajectoireProps { + collectiviteId: number; + identifiantReferentiel: string; + readonly?: boolean; +} + /** Affiche l'encadré "Comparez la trajectoire SNBC à vos objectifs et vos résultats" */ export const ComparezLaTrajectoire = ({ collectiviteId, identifiantReferentiel, -}: { - collectiviteId: number; - identifiantReferentiel: string; -}) => { + readonly = false, +}: ComparezLaTrajectoireProps) => { return (
Comparez la trajectoire SNBC à vos objectifs et vos résultats
-

- Pour cela, il faut d'abord{' '} - compléter vos objectifs et vos résultats dans vos Indicateurs. - Vous avez le choix d'appliquer les données disponibles en open - data, ou bien de renseigner vos propres données. -

+ {readonly ? ( +

+ Pour cela, il faut d'abord{' '} + + faire compléter vos objectifs et vos résultats dans vos Indicateurs + par un utilisateur en Edition ou Admin sur le profil de cette + collectivité + + . L'utilisateur pourra appliquer les données disponibles en open + data, ou bien renseigner ses propres données. +

+ ) : ( +

+ Pour cela, il faut d'abord{' '} + compléter vos objectifs et vos résultats dans vos Indicateurs. + Vous avez le choix d'appliquer les données disponibles en open + data, ou bien de renseigner vos propres données. +

+ )} + } /> diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/Trajectoire/Trajectoire.tsx b/app.territoiresentransitions.react/src/app/pages/collectivite/Trajectoire/Trajectoire.tsx index db0b81ef3a..8edb7d778e 100644 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/Trajectoire/Trajectoire.tsx +++ b/app.territoiresentransitions.react/src/app/pages/collectivite/Trajectoire/Trajectoire.tsx @@ -3,6 +3,7 @@ import SpinnerLoader from '@/app/ui/shared/SpinnerLoader'; import { Alert, Button, Card, Modal, TrackPageView } from '@/ui'; import { useEffect } from 'react'; import { useQueryClient } from 'react-query'; +import { useCurrentCollectivite } from '../../../../core-logic/hooks/useCurrentCollectivite'; import { CommuneNonSupportee } from './CommuneNonSupportee'; import { HELPDESK_URL } from './constants'; import { ReactComponent as DbErrorPicto } from './db-error.svg'; @@ -63,23 +64,45 @@ const TrajectoireContent = (props: { * ne sont pas disponibles. */ const DonneesNonDispo = () => { + const collectivite = useCurrentCollectivite(); + return (

Données disponibles insuffisantes pour le calcul

-

- Nous ne disposons pas encore des données suffisantes pour permettre le - calcul automatique de la trajectoire SNBC territorialisée de votre - collectivité. Vous pouvez néanmoins lancer un calcul en complétant les - données disponibles en open data avec vos propres données. Vous pourrez - ainsi visualiser facilement votre trajectoire SNBC territorialisée et la - comparer aux objectifs fixés et résultats observés. -

+ {collectivite?.readonly ? ( +

+ Nous ne disposons pas encore des données suffisantes pour permettre le + calcul automatique de la trajectoire SNBC territorialisé de votre + collectivité.{' '} + + Un utilisateur en Edition ou Admin sur le profil de cette + collectivité + {' '} + peut néanmoins lancer un calcul en complétant les données disponibles + en open data avec celles disponibles au sein de la collectivité. Vous + pourrez ensuite visualiser facilement votre trajectoire SNBC + territorialisée et la comparer aux objectifs fixés et résultats + observés. +

+ ) : ( +

+ Nous ne disposons pas encore des données suffisantes pour permettre le + calcul automatique de la trajectoire SNBC territorialisée de votre + collectivité. Vous pouvez néanmoins lancer un calcul en complétant les + données disponibles en open data avec vos propres données. Vous + pourrez ainsi visualiser facilement votre trajectoire SNBC + territorialisée et la comparer aux objectifs fixés et résultats + observés. +

+ )} } > - +
); diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/Trajectoire/TrajectoireCalculee.tsx b/app.territoiresentransitions.react/src/app/pages/collectivite/Trajectoire/TrajectoireCalculee.tsx index 39367fb458..e3906dc4cf 100644 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/Trajectoire/TrajectoireCalculee.tsx +++ b/app.territoiresentransitions.react/src/app/pages/collectivite/Trajectoire/TrajectoireCalculee.tsx @@ -134,7 +134,14 @@ export const TrajectoireCalculee = () => { { /** Avertissement "Données partiellement disponibles" */ !secteur && donneesSectoriellesIncompletes && ( - + ) } { @@ -187,7 +194,12 @@ export const TrajectoireCalculee = () => { secteur && !valeursSecteur && ( ) } @@ -201,6 +213,7 @@ export const TrajectoireCalculee = () => { )} {secteur && } diff --git a/app.territoiresentransitions.react/src/core-logic/api/auth/AuthProvider.tsx b/app.territoiresentransitions.react/src/core-logic/api/auth/AuthProvider.tsx index 250c5dfb1c..1869c9ef4c 100644 --- a/app.territoiresentransitions.react/src/core-logic/api/auth/AuthProvider.tsx +++ b/app.territoiresentransitions.react/src/core-logic/api/auth/AuthProvider.tsx @@ -14,6 +14,7 @@ import { SignInWithPasswordCredentials, User, } from '@supabase/supabase-js'; +import { usePostHog } from 'posthog-js/react'; import { createContext, ReactNode, @@ -109,6 +110,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const { data: userData = null } = useUserData(session); + const posthog = usePostHog(); + useEffect(() => { // écoute les changements d'état (connecté, déconnecté, etc.) const { @@ -139,12 +142,22 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { if (window.location.href.includes('localhost')) environment = 'development'; + if (posthog) { + posthog.identify(userData.id, { + email: userData.email, + user_id: userData.id, + }); + } + if (environment === 'production' || environment === 'test') { // @ts-expect-error - StonlyWidget is not defined window.StonlyWidget('identify', userData.user_id); } } else { clearCrispUserData(); + if (posthog) { + posthog.reset(); + } } }, [userData]); diff --git a/backend/src/auth/authorizations/permission.models.ts b/backend/src/auth/authorizations/permission.models.ts index 2145c887c0..f208f300c8 100644 --- a/backend/src/auth/authorizations/permission.models.ts +++ b/backend/src/auth/authorizations/permission.models.ts @@ -17,12 +17,14 @@ export const Permission: Record = { PermissionOperation.INDICATEURS_VISITE, PermissionOperation.INDICATEURS_LECTURE, PermissionOperation.INDICATEURS_TRAJECTOIRES_LECTURE, + PermissionOperation.INDICATEURS_TRAJECTOIRES_EDITION, ], [Role.ADEME]: [ PermissionOperation.COLLECTIVITES_VISITE, PermissionOperation.PLANS_FICHES_VISITE, PermissionOperation.INDICATEURS_VISITE, PermissionOperation.INDICATEURS_TRAJECTOIRES_LECTURE, + PermissionOperation.INDICATEURS_TRAJECTOIRES_EDITION, ], [Role.LECTURE]: [ PermissionOperation.COLLECTIVITES_VISITE, @@ -33,6 +35,7 @@ export const Permission: Record = { PermissionOperation.INDICATEURS_VISITE, PermissionOperation.INDICATEURS_LECTURE, PermissionOperation.INDICATEURS_TRAJECTOIRES_LECTURE, + PermissionOperation.INDICATEURS_TRAJECTOIRES_EDITION, ], [Role.EDITION]: [ PermissionOperation.COLLECTIVITES_VISITE, diff --git a/backend/src/auth/authorizations/roles/role.service.ts b/backend/src/auth/authorizations/roles/role.service.ts index a445a362a0..fa5bae47c1 100644 --- a/backend/src/auth/authorizations/roles/role.service.ts +++ b/backend/src/auth/authorizations/roles/role.service.ts @@ -117,7 +117,7 @@ export class RoleService { .select() .from(utilisateurSupportTable) .where(eq(utilisateurSupportTable.userId, userId)); - return result[0].support || false; + return (result.length && result[0]?.support) || false; } /** diff --git a/backend/src/indicateurs/trajectoires/trajectoire-snbc.e2e-spec.ts b/backend/src/indicateurs/trajectoires/trajectoire-snbc.e2e-spec.ts index 506abcfff0..a07b3626d0 100644 --- a/backend/src/indicateurs/trajectoires/trajectoire-snbc.e2e-spec.ts +++ b/backend/src/indicateurs/trajectoires/trajectoire-snbc.e2e-spec.ts @@ -282,19 +282,26 @@ describe('Calcul de trajectoire SNBC', () => { }); }, 10000); - it(`Calcul sans droit suffisant (uniquement lecture)`, () => { + it(`Calcul sans droit suffisant - visite`, () => { return request(app.getHttpServer()) - .get('/trajectoires/snbc?collectiviteId=3895') + .get('/trajectoires/snbc?collectiviteId=3896') .set('Authorization', `Bearer ${yoloDodoToken}`) .expect(401) .expect({ message: - "Droits insuffisants, l'utilisateur 17440546-f389-4d4f-bfdb-b0c94a1bd0f9 n'a pas l'autorisation indicateurs.trajectoires.edition sur la ressource Collectivité 3895", + "Droits insuffisants, l'utilisateur 17440546-f389-4d4f-bfdb-b0c94a1bd0f9 n'a pas l'autorisation indicateurs.trajectoires.lecture sur la ressource Collectivité 3896", error: 'Unauthorized', statusCode: 401, }); }); + it(`Calcul avec droit suffisant - lecture`, async () => { + return request(app.getHttpServer()) + .get('/trajectoires/snbc?collectiviteId=3895') + .set('Authorization', `Bearer ${yoloDodoToken}`) + .expect(200); + }, 30000); + it(`Verification et calcul avec donnees completes`, async () => { // Suppression de la trajectoire snbc existante si le test est joué plusieurs fois await request(app.getHttpServer()) diff --git a/backend/src/indicateurs/trajectoires/trajectoires-spreadsheet.service.ts b/backend/src/indicateurs/trajectoires/trajectoires-spreadsheet.service.ts index 56d7719e8c..5f12294b2c 100644 --- a/backend/src/indicateurs/trajectoires/trajectoires-spreadsheet.service.ts +++ b/backend/src/indicateurs/trajectoires/trajectoires-spreadsheet.service.ts @@ -324,7 +324,7 @@ export default class TrajectoiresSpreadsheetService { const upsertedTrajectoireIndicateurValeurs = await this.valeursService.upsertIndicateurValeurs( indicateurValeursTrajectoireResultat, - tokenInfo + undefined // we don't want to check permission, we have already checked it and it's not the same ); // Maintenant que les indicateurs ont été créés, on peut ajouter la collectivité au groupement diff --git a/backend/src/indicateurs/valeurs/crud-valeurs.service.ts b/backend/src/indicateurs/valeurs/crud-valeurs.service.ts index 9b142108c6..df75c0a60a 100644 --- a/backend/src/indicateurs/valeurs/crud-valeurs.service.ts +++ b/backend/src/indicateurs/valeurs/crud-valeurs.service.ts @@ -307,7 +307,7 @@ export default class CrudValeursService { async upsertIndicateurValeurs( indicateurValeurs: IndicateurValeurInsert[], - tokenInfo: AuthenticatedUser + tokenInfo: AuthenticatedUser | undefined ): Promise { if (tokenInfo) { const collectiviteIds = [ @@ -331,7 +331,7 @@ export default class CrudValeursService { } this.logger.log( - `Upsert des ${indicateurValeurs.length} valeurs des indicateurs pour l'utilisateur ${tokenInfo.id} (role ${tokenInfo.role})` + `Upsert des ${indicateurValeurs.length} valeurs des indicateurs pour l'utilisateur ${tokenInfo?.id} (role ${tokenInfo?.role})` ); // On doit distinguer les valeurs avec et sans métadonnées car la clause d'unicité est différente (onConflictDoUpdate) const [indicateurValeursAvecMetadonnees, indicateurValeursSansMetadonnees] =