Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getCandidature } from '@/app/_helpers';
import { ProjetBannerTemplate } from '@/components/molecules/projet/ProjetBanner.template';
import { StatutCandidatureBadge } from '@/components/molecules/candidature/StatutCandidatureBadge';
import { NotificationBadge } from '@/components/molecules/candidature/NotificationBadge';
import { PageWithErrorHandling } from '@/utils/PageWithErrorHandling';

type LayoutProps = {
children: React.ReactNode;
Expand Down Expand Up @@ -40,29 +41,31 @@ export default async function CandidatureLayout({
children,
params: { identifiant },
}: LayoutProps) {
const identifiantProjetValue = decodeParameter(identifiant);
const { identifiantProjet, notification, dépôt, instruction } =
await getCandidature(identifiantProjetValue);
return PageWithErrorHandling(async () => {
const identifiantProjetValue = decodeParameter(identifiant);
const { identifiantProjet, notification, dépôt, instruction } =
await getCandidature(identifiantProjetValue);

return (
<PageTemplate
banner={
<ProjetBannerTemplate
identifiantProjet={identifiantProjet}
href={notification ? Routes.Projet.details(identifiantProjet.formatter()) : undefined}
nom={dépôt.nomProjet}
localité={dépôt.localité}
badge={
<div className="flex gap-2">
<StatutCandidatureBadge statut={instruction.statut.statut} />
<NotificationBadge estNotifié={!!notification} />
</div>
}
dateDésignation={notification ? notification.notifiéeLe.formatter() : Option.none}
/>
}
>
{children}
</PageTemplate>
);
return (
<PageTemplate
banner={
<ProjetBannerTemplate
identifiantProjet={identifiantProjet}
href={notification ? Routes.Projet.details(identifiantProjet.formatter()) : undefined}
nom={dépôt.nomProjet}
localité={dépôt.localité}
badge={
<div className="flex gap-2">
<StatutCandidatureBadge statut={instruction.statut.statut} />
<NotificationBadge estNotifié={!!notification} />
</div>
}
dateDésignation={notification ? notification.notifiéeLe.formatter() : Option.none}
/>
}
>
{children}
</PageTemplate>
);
});
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Metadata, ResolvingMetadata } from 'next';
import { notFound } from 'next/navigation';

import { IdentifiantProjet } from '@potentiel-domain/projet';
import { mapToPlainObject } from '@potentiel-domain/core';

import { ProjetÉliminéBanner } from '@/components/molecules/projet/éliminé/ProjetÉliminéBanner';
import { PageTemplate } from '@/components/templates/Page.template';
import { decodeParameter } from '@/utils/decodeParameter';
import { IdentifiantParameter } from '@/utils/identifiantParameter';
import { getÉliminé } from '@/app/_helpers/getÉliminé';
import { PageWithErrorHandling } from '@/utils/PageWithErrorHandling';
import { getLauréatInfos } from '@/app/laureats/[identifiant]/_helpers';
import { ProjetLauréatBanner } from '@/components/molecules/projet/lauréat/ProjetLauréatBanner';

type LayoutProps = IdentifiantParameter & {
children: React.ReactNode;
Expand Down Expand Up @@ -34,11 +40,34 @@ export async function generateMetadata(
}
}

export default function ÉliminéLayout({ children, params: { identifiant } }: LayoutProps) {
const identifiantProjet = decodeParameter(identifiant);
return (
<PageTemplate banner={<ProjetÉliminéBanner identifiantProjet={identifiantProjet} />}>
{children}
</PageTemplate>
);
export default async function ÉliminéLayout({ children, params: { identifiant } }: LayoutProps) {
return PageWithErrorHandling(async () => {
const identifiantProjet = decodeParameter(identifiant);
const éliminé = await getÉliminé(identifiantProjet);

// dans le cas d'un recours accordé, le projet devient lauréat
if (!éliminé) {
const lauréat = await getLauréatInfos(
IdentifiantProjet.convertirEnValueType(identifiantProjet).formatter(),
);
return (
<ProjetLauréatBanner
identifiantProjet={identifiantProjet}
projet={mapToPlainObject(lauréat)}
/>
);
}
return (
<PageTemplate
banner={
<ProjetÉliminéBanner
identifiantProjet={identifiantProjet}
projet={mapToPlainObject(éliminé)}
/>
}
>
{children}
</PageTemplate>
);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const CahierDesChargesSection = ({
const cahierDesCharges = await getCahierDesCharges(identifiantProjet.formatter());

const cahierDesChargesModificatifDisponible =
rôle.aLaPermission('cahierDesCharges.choisir') &&
cahierDesCharges.période.cahiersDesChargesModifiésDisponibles.length;

const doitChoisirUnCahierDesChargesModificatif =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Metadata, ResolvingMetadata } from 'next';

import { IdentifiantProjet } from '@potentiel-domain/projet';
import { mapToPlainObject } from '@potentiel-domain/core';

import { ProjetLauréatBanner } from '@/components/molecules/projet/lauréat/ProjetLauréatBanner';
import { PageTemplate } from '@/components/templates/Page.template';
import { decodeParameter } from '@/utils/decodeParameter';
import { IdentifiantParameter } from '@/utils/identifiantParameter';
import { PageWithErrorHandling } from '@/utils/PageWithErrorHandling';

import { getLauréatInfos } from './_helpers/getLauréat';

Expand Down Expand Up @@ -37,10 +39,22 @@ export async function generateMetadata(
}

export default function LauréatLayout({ children, params: { identifiant } }: LayoutProps) {
const identifiantProjet = decodeParameter(identifiant);
return (
<PageTemplate banner={<ProjetLauréatBanner identifiantProjet={identifiantProjet} />}>
{children}
</PageTemplate>
);
return PageWithErrorHandling(async () => {
const identifiantProjet = decodeParameter(identifiant);
const projet = await getLauréatInfos(
IdentifiantProjet.convertirEnValueType(identifiantProjet).formatter(),
);
return (
<PageTemplate
banner={
<ProjetLauréatBanner
identifiantProjet={identifiantProjet}
projet={mapToPlainObject(projet)}
/>
}
>
{children}
</PageTemplate>
);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const ProjetBannerTemplate: FC<ProjetBannerProps> = ({
) : (
<p className="text-xl font-bold !text-theme-white mr-2">{nom}</p>
)}
<div className="hidden print:block">{badge}</div>
<div>{badge}</div>
{process.env.APPLICATION_STAGE !== 'production' && (
<CopyButton
textToCopy={identifiantProjet.formatter()}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
'use server';

import { FC } from 'react';

import { Routes } from '@potentiel-applications/routes';
import { IdentifiantProjet } from '@potentiel-domain/projet';
import { IdentifiantProjet, Lauréat } from '@potentiel-domain/projet';
import { Option } from '@potentiel-libraries/monads';
import { PlainType } from '@potentiel-domain/core';

import { withUtilisateur } from '@/utils/withUtilisateur';
import { getLauréatInfos } from '@/app/laureats/[identifiant]/_helpers/getLauréat';

import { ProjetBannerTemplate } from '../ProjetBanner.template';

Expand All @@ -16,25 +14,23 @@ import { StatutLauréatBadge } from './StatutLauréatBadge';
export type ProjetLauréatBannerProps = {
identifiantProjet: string;
noLink?: true;
projet: PlainType<Lauréat.ConsulterLauréatReadModel>;
};

export const ProjetLauréatBanner: FC<ProjetLauréatBannerProps> = async ({
export const ProjetLauréatBanner: FC<ProjetLauréatBannerProps> = ({
identifiantProjet,
noLink,
projet,
}) =>
withUtilisateur(async ({ rôle }) => {
const projet = await getLauréatInfos(
IdentifiantProjet.convertirEnValueType(identifiantProjet).formatter(),
);

const { nomProjet, localité, notifiéLe, statut } = projet;

return (
<ProjetBannerTemplate
badge={<StatutLauréatBadge statut={statut.statut} />}
localité={localité}
dateDésignation={Option.match(notifiéLe)
.some((date) => date.formatter())
.some((date) => date.date)
.none()}
/***
* @todo changer le check du rôle quand la page projet sera matérialisée dans le SSR (utiliser rôle.aLaPermissionDe)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,36 @@
'use server';

import { FC } from 'react';

import { Routes } from '@potentiel-applications/routes';
import { IdentifiantProjet } from '@potentiel-domain/projet';
import { IdentifiantProjet, Éliminé } from '@potentiel-domain/projet';
import { Option } from '@potentiel-libraries/monads';
import { PlainType } from '@potentiel-domain/core';

import { withUtilisateur } from '@/utils/withUtilisateur';
import { getÉliminé } from '@/app/_helpers/getÉliminé';

import { ProjetBannerTemplate } from '../ProjetBanner.template';
import { ProjetLauréatBanner } from '../lauréat/ProjetLauréatBanner';

import { StatutÉliminéBadge } from './StatutÉliminéBadge';

export type ProjetÉliminéBannerProps = {
identifiantProjet: string;
noLink?: true;
projet: PlainType<Éliminé.ConsulterÉliminéReadModel>;
};

export const ProjetÉliminéBanner: FC<ProjetÉliminéBannerProps> = async ({
export const ProjetÉliminéBanner: FC<ProjetÉliminéBannerProps> = ({
identifiantProjet,
noLink,
projet,
}) =>
withUtilisateur(async ({ rôle }) => {
const projet = await getÉliminé(identifiantProjet);

// dans le cas d'un recours accordé, le projet devient lauréat
if (!projet) {
return <ProjetLauréatBanner identifiantProjet={identifiantProjet} />;
}

const { nomProjet, localité, notifiéLe } = projet;

return (
<ProjetBannerTemplate
badge={<StatutÉliminéBadge />}
localité={localité}
dateDésignation={Option.match(notifiéLe)
.some((date) => date.formatter())
.some((date) => date.date)
.none()}
/***
* @todo changer le check du rôle quand la page projet sera matérialisée dans le SSR (utiliser rôle.aLaPermissionDe)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const InviterPorteurForm: FC<InviterPorteurFormProps> = ({
return (
<>
{peutInviter && (
<Button iconId="fr-icon-user-line" className="w-full" onClick={() => setIsOpen(true)}>
<Button iconId="fr-icon-user-line" onClick={() => setIsOpen(true)}>
Inviter un nouvel utilisateur
</Button>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const AccèsListPage: FC<AccèsListPageProps> = ({
)}
</Section>
</div>
<div className="flex flex-1 flex-col gap-4 items-center">
<div className="flex flex-1 flex-col gap-4">
<InviterPorteurForm
identifiantProjet={identifiantProjet}
nombreDeProjets={nombreDeProjets}
Expand All @@ -56,7 +56,6 @@ export const AccèsListPage: FC<AccèsListPageProps> = ({
<Button
iconId="fr-icon-mail-line"
priority="secondary"
className="w-full"
linkProps={{
href: `mailto:${accès.map((item) => item.identifiantUtilisateur).join(',')}`,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,10 @@ Alors(`la candidature devrait être consultable`, async function (this: Potentie
Alors(
'le porteur a été prévenu que son attestation a été modifiée',
async function (this: PotentielWorld) {
const email = this.notificationWorld.récupérerNotification(
this.notificationWorld.vérifierNotification(
this.candidatureWorld.importerCandidature.values.emailContactValue,
'Potentiel - Une nouvelle attestation est disponible pour le projet .*',
);

await waitForExpect(async () => {
expect(email.messageSubject).match(
/Potentiel - Une nouvelle attestation est disponible pour le projet .*/,
);
});
},
);

Expand All @@ -78,7 +73,7 @@ Alors(
// edge case because we want to wait for a notification that should not be sent
await sleep(400);
try {
this.notificationWorld.récupérerNotification(
this.notificationWorld.vérifierNotification(
this.candidatureWorld.importerCandidature.values.emailContactValue,
);
} catch (error) {
Expand Down
24 changes: 21 additions & 3 deletions packages/specifications/src/notification/notification.world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export class NotificationWorld {
}
}

récupérerNotification(emailValue: string, sujet?: string) {
vérifierNotification(emailValue: string, sujet?: string, variables?: Record<string, string>) {
const logger = getLogger('NotificationWorld');
const email = Email.convertirEnValueType(emailValue);
const notif = this.#notifications.find((notif) => {
if (notif.checked) {
Expand All @@ -28,10 +29,28 @@ export class NotificationWorld {
if (sujet && !notif.messageSubject.match(new RegExp(sujet))) {
return false;
}
if (variables) {
for (const [key, value] of Object.entries(variables)) {
if (!new RegExp(value).test(notif.variables[key])) {
logger.debug(
"Une notification correspond au sujet et à l'email, mais pas aux variables",
{
key,
expected: value,
actual: notif.variables[key],
email: email.formatter(),
sujet,
},
);

return false;
}
}
}
return notif.email.estÉgaleÀ(email);
});
if (!notif) {
getLogger('NotificationWorld').debug(`Aucune notification trouvée`, {
logger.debug(`Aucune notification trouvée`, {
sujet,
emailValue,
notificationsEnvoyées: this.#notifications.map((x) => ({
Expand All @@ -43,7 +62,6 @@ export class NotificationWorld {
assert(notif, 'Pas de notification');

notif.checked = true;
return notif;
}

resetNotifications() {
Expand Down
Loading
Loading