From 041145f86559abda4048e49a4fe66c44ad386e79 Mon Sep 17 00:00:00 2001 From: Hubert MONCENIS Date: Fri, 9 Jan 2026 14:15:34 +0100 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=9A=A7=20Basic=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ssr/src/app/export/exportProjet.action.ts | 27 +++++++++++++++++++ .../ssr/src/app/export/exportProjet.form.tsx | 16 +++++++++++ .../ssr/src/app/export/exportProjet.page.tsx | 16 +++++++++++ .../domain/utilisateur/src/role.valueType.ts | 13 +++++++++ 4 files changed, 72 insertions(+) create mode 100644 packages/applications/ssr/src/app/export/exportProjet.action.ts create mode 100644 packages/applications/ssr/src/app/export/exportProjet.form.tsx create mode 100644 packages/applications/ssr/src/app/export/exportProjet.page.tsx diff --git a/packages/applications/ssr/src/app/export/exportProjet.action.ts b/packages/applications/ssr/src/app/export/exportProjet.action.ts new file mode 100644 index 0000000000..2b7161fd00 --- /dev/null +++ b/packages/applications/ssr/src/app/export/exportProjet.action.ts @@ -0,0 +1,27 @@ +'use server'; + +import zod from 'zod'; + +import { formAction, FormAction, FormState } from '@/utils/formAction'; +import { withUtilisateur } from '@/utils/withUtilisateur'; + +export type ExportProjetState = FormState; + +const schema = zod.object({ + typeExport: zod.enum(['raccordement', 'fournisseur', 'global']), +}); + +export type ExportProjetFormKeys = keyof zod.infer; + +const action: FormAction = async (_, { typeExport }) => + withUtilisateur(async (_) => { + // Implémentation de l'export en fonction du typeExport + // Par exemple, générer un fichier CSV et le retourner + + return { + status: 'success', + message: `Export de type ${typeExport} réalisé avec succès.`, + }; + }); + +export const exportProjetAction = formAction(action, schema); diff --git a/packages/applications/ssr/src/app/export/exportProjet.form.tsx b/packages/applications/ssr/src/app/export/exportProjet.form.tsx new file mode 100644 index 0000000000..e64a032ead --- /dev/null +++ b/packages/applications/ssr/src/app/export/exportProjet.form.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react'; + +import { Form } from '@/components/atoms/form/Form'; + +import { exportProjetAction } from './exportProjet.action'; + +export const ExportProjetForm: FC = () => ( +
+ +
+); diff --git a/packages/applications/ssr/src/app/export/exportProjet.page.tsx b/packages/applications/ssr/src/app/export/exportProjet.page.tsx new file mode 100644 index 0000000000..0945775933 --- /dev/null +++ b/packages/applications/ssr/src/app/export/exportProjet.page.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react'; + +import { PageTemplate } from '@/components/templates/Page.template'; +import { Heading1 } from '@/components/atoms/headings'; + +import { ExportProjetForm } from './exportProjet.form'; + +export const ExportProjetPage: FC = () => ( + Exporter des données projets}> +

+ Cette page permet d'exporter les données des projets, vous devez tout d'abord sélectionner le + type d'export désiré +

+ +
+); diff --git a/packages/domain/utilisateur/src/role.valueType.ts b/packages/domain/utilisateur/src/role.valueType.ts index 5bd1eb5449..90b4af55b7 100644 --- a/packages/domain/utilisateur/src/role.valueType.ts +++ b/packages/domain/utilisateur/src/role.valueType.ts @@ -649,6 +649,13 @@ const référencielPermissions = { lister: 'Lauréat.Query.ListerHistoriqueProjet', }, }, + projet: { + export: { + query: { + exportRaccordement: 'Projet.Query.ExportRaccordement', + }, + }, + }, } as const; /** @@ -1535,6 +1542,9 @@ const policies = { accèsDonnées: { prix: [], }, + export: { + exportRaccordement: [référencielPermissions.projet.export.query.exportRaccordement], + }, }, appelOffre: { consulter: [référencielPermissions.appelOffre.query.consulter], @@ -1767,6 +1777,9 @@ const adminPolicies: ReadonlyArray = [ // Tâche 'tâche.consulter', + + // Export projet + 'projet.export.exportRaccordement', ]; const dgecValidateurPolicies: ReadonlyArray = [ From 337fbe694c269bfcaa06663f3bc47d5d097bb792 Mon Sep 17 00:00:00 2001 From: Hubert MONCENIS Date: Sat, 10 Jan 2026 09:00:32 +0100 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=8E=A8=20Cr=C3=A9ation=20d'une=20rout?= =?UTF-8?q?e=20pour=20l'export=20projet=20raccordement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ssr/src/app/export/exportProjet.action.ts | 27 ------------------- .../ssr/src/app/export/exportProjet.form.tsx | 16 ----------- .../ssr/src/app/export/exportProjet.page.tsx | 17 +++++++----- 3 files changed, 10 insertions(+), 50 deletions(-) delete mode 100644 packages/applications/ssr/src/app/export/exportProjet.action.ts delete mode 100644 packages/applications/ssr/src/app/export/exportProjet.form.tsx diff --git a/packages/applications/ssr/src/app/export/exportProjet.action.ts b/packages/applications/ssr/src/app/export/exportProjet.action.ts deleted file mode 100644 index 2b7161fd00..0000000000 --- a/packages/applications/ssr/src/app/export/exportProjet.action.ts +++ /dev/null @@ -1,27 +0,0 @@ -'use server'; - -import zod from 'zod'; - -import { formAction, FormAction, FormState } from '@/utils/formAction'; -import { withUtilisateur } from '@/utils/withUtilisateur'; - -export type ExportProjetState = FormState; - -const schema = zod.object({ - typeExport: zod.enum(['raccordement', 'fournisseur', 'global']), -}); - -export type ExportProjetFormKeys = keyof zod.infer; - -const action: FormAction = async (_, { typeExport }) => - withUtilisateur(async (_) => { - // Implémentation de l'export en fonction du typeExport - // Par exemple, générer un fichier CSV et le retourner - - return { - status: 'success', - message: `Export de type ${typeExport} réalisé avec succès.`, - }; - }); - -export const exportProjetAction = formAction(action, schema); diff --git a/packages/applications/ssr/src/app/export/exportProjet.form.tsx b/packages/applications/ssr/src/app/export/exportProjet.form.tsx deleted file mode 100644 index e64a032ead..0000000000 --- a/packages/applications/ssr/src/app/export/exportProjet.form.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { FC } from 'react'; - -import { Form } from '@/components/atoms/form/Form'; - -import { exportProjetAction } from './exportProjet.action'; - -export const ExportProjetForm: FC = () => ( -
- -
-); diff --git a/packages/applications/ssr/src/app/export/exportProjet.page.tsx b/packages/applications/ssr/src/app/export/exportProjet.page.tsx index 0945775933..ab50899881 100644 --- a/packages/applications/ssr/src/app/export/exportProjet.page.tsx +++ b/packages/applications/ssr/src/app/export/exportProjet.page.tsx @@ -1,16 +1,19 @@ import { FC } from 'react'; +import { Routes } from '@potentiel-applications/routes'; + import { PageTemplate } from '@/components/templates/Page.template'; import { Heading1 } from '@/components/atoms/headings'; - -import { ExportProjetForm } from './exportProjet.form'; +import { LinkAction } from '@/components/atoms/LinkAction'; export const ExportProjetPage: FC = () => ( Exporter des données projets}> -

- Cette page permet d'exporter les données des projets, vous devez tout d'abord sélectionner le - type d'export désiré -

- +
+ Cette page permet d'accèder à des liens pour exporter des données des projets +
+
); From 5e712d7b9797c811b9e15e64006b3d347ab27fbb Mon Sep 17 00:00:00 2001 From: Hubert MONCENIS Date: Sat, 10 Jan 2026 11:27:04 +0100 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=8E=A8=20Export=20projet=20d=C3=A9tai?= =?UTF-8?q?l=20des=20fournisseurs=20d'une=20candidature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../load-test/notifier-candidatures.ts | 1 + .../src/candidature/candidature.routes.ts | 2 + ...idature-r\303\251f\303\251rentiel-dgec.ts" | 7 +- .../ssr/src/app/candidatures/(liste)/page.tsx | 204 +++++++++--------- .../candidatures/export-fournisseurs/route.ts | 61 ++++++ .../ssr/src/app/export/exportProjet.page.tsx | 16 +- .../notifierP\303\251riode.action.ts" | 1 + .../ssr/src/app/periodes/page.tsx | 14 +- .../lister/listerCandidatures.query.ts | 23 +- .../domain/utilisateur/src/role.valueType.ts | 4 + .../stepDefinitions/p\303\251riode.then.ts" | 6 + 11 files changed, 230 insertions(+), 109 deletions(-) create mode 100644 packages/applications/ssr/src/app/candidatures/export-fournisseurs/route.ts diff --git a/packages/applications/cli/src/commands/load-test/notifier-candidatures.ts b/packages/applications/cli/src/commands/load-test/notifier-candidatures.ts index a823bf700c..d901429edb 100644 --- a/packages/applications/cli/src/commands/load-test/notifier-candidatures.ts +++ b/packages/applications/cli/src/commands/load-test/notifier-candidatures.ts @@ -71,6 +71,7 @@ export class NotifierCandidatures extends Command { const candidatures = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { + utilisateur: Email.système.formatter(), appelOffre, période: String(periode), estNotifiée: false, diff --git a/packages/applications/routes/src/candidature/candidature.routes.ts b/packages/applications/routes/src/candidature/candidature.routes.ts index fc951b4e7e..dd9a16251e 100644 --- a/packages/applications/routes/src/candidature/candidature.routes.ts +++ b/packages/applications/routes/src/candidature/candidature.routes.ts @@ -56,3 +56,5 @@ export const prévisualiserAttestation = _avecIdentifiant('/previsualiser-attest // TODO: à supprimer pour utiliser directement Routes.Document.télécharger dans le front // une fois qu'on aura migré la page Projet export const téléchargerAttestation = _avecIdentifiant('/telecharger-attestation'); + +export const exporterFournisseurs = '/candidatures/export-fournisseurs'; diff --git "a/packages/applications/scheduled-tasks/src/garanties-financi\303\250res/r\303\251cup\303\251rer-fichier-gf-candidature-r\303\251f\303\251rentiel-dgec.ts" "b/packages/applications/scheduled-tasks/src/garanties-financi\303\250res/r\303\251cup\303\251rer-fichier-gf-candidature-r\303\251f\303\251rentiel-dgec.ts" index 458b83e9c8..b2da3ae879 100644 --- "a/packages/applications/scheduled-tasks/src/garanties-financi\303\250res/r\303\251cup\303\251rer-fichier-gf-candidature-r\303\251f\303\251rentiel-dgec.ts" +++ "b/packages/applications/scheduled-tasks/src/garanties-financi\303\250res/r\303\251cup\303\251rer-fichier-gf-candidature-r\303\251f\303\251rentiel-dgec.ts" @@ -9,7 +9,11 @@ import { Option } from '@potentiel-libraries/monads'; import { DateTime, Email } from '@potentiel-domain/common'; import { findProjection, listProjection } from '@potentiel-infrastructure/pg-projection-read'; import { Document, IdentifiantProjet, Candidature, Lauréat } from '@potentiel-domain/projet'; -import { ProjetAdapter, DocumentAdapter } from '@potentiel-infrastructure/domain-adapters'; +import { + ProjetAdapter, + DocumentAdapter, + getProjetUtilisateurScopeAdapter, +} from '@potentiel-infrastructure/domain-adapters'; import { Période } from '@potentiel-domain/periode'; export const dgecEmail = 'aopv.dgec@developpement-durable.gouv.fr'; @@ -38,6 +42,7 @@ Candidature.registerCandidatureQueries({ list: listProjection, récupérerProjetsEligiblesPreuveRecanditure: ProjetAdapter.récupérerProjetsEligiblesPreuveRecanditureAdapter, + getScopeProjetUtilisateur: getProjetUtilisateurScopeAdapter, }); Document.registerDocumentProjetCommand({ diff --git a/packages/applications/ssr/src/app/candidatures/(liste)/page.tsx b/packages/applications/ssr/src/app/candidatures/(liste)/page.tsx index 5af25bc68e..69213f98e1 100644 --- a/packages/applications/ssr/src/app/candidatures/(liste)/page.tsx +++ b/packages/applications/ssr/src/app/candidatures/(liste)/page.tsx @@ -14,6 +14,7 @@ import { ListFilterItem } from '@/components/molecules/ListFilters'; import { transformToOptionalEnumArray } from '@/app/_helpers/transformToOptionalStringArray'; import { getTypeActionnariatFilterOptions } from '@/app/_helpers/filters/getTypeActionnariatFilterOptions'; import { candidatureListLegendSymbols } from '@/components/molecules/candidature/CandidatureListLegendAndSymbols'; +import { withUtilisateur } from '@/utils/withUtilisateur'; import { CandidatureListPage } from './CandidatureList.page'; @@ -43,104 +44,107 @@ const paramsSchema = z.object({ type SearchParams = keyof z.infer; export default async function Page({ searchParams }: PageProps) { - return PageWithErrorHandling(async () => { - const { page, appelOffre, famille, nomProjet, periode, statut, notifie, typeActionnariat } = - paramsSchema.parse(searchParams); - - if (nomProjet && IdentifiantProjet.estValide(nomProjet)) { - return redirect(Routes.Candidature.détails(nomProjet)); - } - - const candidatures = await mediator.send({ - type: 'Candidature.Query.ListerCandidatures', - data: { - range: mapToRangeOptions({ - currentPage: page, - itemsPerPage: 10, - }), - nomProjet, - appelOffre, - période: periode, - famille, - statut, - typeActionnariat, - estNotifiée: notifie, - }, - }); - - const appelOffres = await mediator.send({ - type: 'AppelOffre.Query.ListerAppelOffre', - data: {}, - }); - - const appelOffresFiltré = appelOffres.items.find((a) => a.id === appelOffre); - - const périodeFiltrée = appelOffresFiltré?.periodes.find((p) => p.id === periode); - - const périodeOptions = - appelOffresFiltré?.periodes.map(({ title, id }) => ({ label: title, value: id })) ?? []; - - const familleOptions = - périodeFiltrée?.familles.map(({ title, id }) => ({ label: title, value: id })) ?? []; - - const filters: ListFilterItem[] = [ - { - label: 'Statut de la candidature', - searchParamKey: 'statut', - options: [ - { label: 'Classé', value: Candidature.StatutCandidature.classé.formatter() }, - { label: 'Éliminé', value: Candidature.StatutCandidature.éliminé.formatter() }, - ], - }, - { - label: 'Statut de la notification', - searchParamKey: 'notifie', - options: [ - { label: 'Notifié', value: 'notifie' }, - { label: 'À notifier', value: 'a-notifier' }, - ], - }, - { - label: `Appel d'offres`, - searchParamKey: 'appelOffre', - options: appelOffres.items.map((appelOffre) => ({ - label: appelOffre.id, - value: appelOffre.id, - })), - affects: ['periode', 'famille'], - }, - { - label: 'Période', - searchParamKey: 'periode', - options: périodeOptions, - affects: ['famille'], - }, - { - label: 'Famille', - searchParamKey: 'famille', - options: familleOptions, - }, - { - label: "Type d'actionnariat", - searchParamKey: 'typeActionnariat', - options: getTypeActionnariatFilterOptions(appelOffresFiltré?.cycleAppelOffre), - multiple: true, - }, - ]; - - return ( - - ); - }); + return PageWithErrorHandling(async () => + withUtilisateur(async (utilisateur) => { + const { page, appelOffre, famille, nomProjet, periode, statut, notifie, typeActionnariat } = + paramsSchema.parse(searchParams); + + if (nomProjet && IdentifiantProjet.estValide(nomProjet)) { + return redirect(Routes.Candidature.détails(nomProjet)); + } + + const candidatures = await mediator.send({ + type: 'Candidature.Query.ListerCandidatures', + data: { + utilisateur: utilisateur.identifiantUtilisateur.email, + range: mapToRangeOptions({ + currentPage: page, + itemsPerPage: 10, + }), + nomProjet, + appelOffre, + période: periode, + famille, + statut, + typeActionnariat, + estNotifiée: notifie, + }, + }); + + const appelOffres = await mediator.send({ + type: 'AppelOffre.Query.ListerAppelOffre', + data: {}, + }); + + const appelOffresFiltré = appelOffres.items.find((a) => a.id === appelOffre); + + const périodeFiltrée = appelOffresFiltré?.periodes.find((p) => p.id === periode); + + const périodeOptions = + appelOffresFiltré?.periodes.map(({ title, id }) => ({ label: title, value: id })) ?? []; + + const familleOptions = + périodeFiltrée?.familles.map(({ title, id }) => ({ label: title, value: id })) ?? []; + + const filters: ListFilterItem[] = [ + { + label: 'Statut de la candidature', + searchParamKey: 'statut', + options: [ + { label: 'Classé', value: Candidature.StatutCandidature.classé.formatter() }, + { label: 'Éliminé', value: Candidature.StatutCandidature.éliminé.formatter() }, + ], + }, + { + label: 'Statut de la notification', + searchParamKey: 'notifie', + options: [ + { label: 'Notifié', value: 'notifie' }, + { label: 'À notifier', value: 'a-notifier' }, + ], + }, + { + label: `Appel d'offres`, + searchParamKey: 'appelOffre', + options: appelOffres.items.map((appelOffre) => ({ + label: appelOffre.id, + value: appelOffre.id, + })), + affects: ['periode', 'famille'], + }, + { + label: 'Période', + searchParamKey: 'periode', + options: périodeOptions, + affects: ['famille'], + }, + { + label: 'Famille', + searchParamKey: 'famille', + options: familleOptions, + }, + { + label: "Type d'actionnariat", + searchParamKey: 'typeActionnariat', + options: getTypeActionnariatFilterOptions(appelOffresFiltré?.cycleAppelOffre), + multiple: true, + }, + ]; + + return ( + + ); + }), + ); } diff --git a/packages/applications/ssr/src/app/candidatures/export-fournisseurs/route.ts b/packages/applications/ssr/src/app/candidatures/export-fournisseurs/route.ts new file mode 100644 index 0000000000..6857b05503 --- /dev/null +++ b/packages/applications/ssr/src/app/candidatures/export-fournisseurs/route.ts @@ -0,0 +1,61 @@ +import { Parser } from '@json2csv/plainjs'; +import { mediator } from 'mediateur'; + +import { Candidature } from '@potentiel-domain/projet'; + +import { apiAction } from '@/utils/apiAction'; +import { typeFournisseurLabel } from '@/app/laureats/[identifiant]/fournisseur/changement/typeFournisseurLabel'; +import { withUtilisateur } from '@/utils/withUtilisateur'; + +export const GET = async (_: Request) => + apiAction(async () => + withUtilisateur(async (utilisateur) => { + const candidatures = await mediator.send({ + type: 'Candidature.Query.ListerCandidatures', + data: { + utilisateur: utilisateur.identifiantUtilisateur.email, + }, + }); + + const data = []; + + for (const candidature of candidatures.items) { + for (const fournisseur of candidature.fournisseurs) { + data.push({ + identifiantProjet: candidature.identifiantProjet.formatter(), + appelOffre: candidature.identifiantProjet.appelOffre, + periode: candidature.identifiantProjet.période, + region: candidature.localité.région, + societeMere: candidature.sociétéMère, + typeFournisseur: typeFournisseurLabel[fournisseur.typeFournisseur.typeFournisseur], + nomDuFabricant: fournisseur.nomDuFabricant, + lieuDeFabrication: fournisseur.lieuDeFabrication, + }); + } + } + + const fields = [ + 'identifiantProjet', + 'appelOffre', + 'periode', + 'region', + 'societeMere', + 'typeFournisseur', + 'nomDuFabricant', + 'lieuDeFabrication', + ]; + + const csvParser = new Parser({ fields, delimiter: ';', withBOM: true }); + + const csv = csvParser.parse(data); + + const fileName = `export_projet_fournisseurs.csv`; + + return new Response(csv, { + headers: { + 'content-type': 'text/csv', + 'content-disposition': `attachment; filename=${fileName}`, + }, + }); + }), + ); diff --git a/packages/applications/ssr/src/app/export/exportProjet.page.tsx b/packages/applications/ssr/src/app/export/exportProjet.page.tsx index ab50899881..ab4d671e12 100644 --- a/packages/applications/ssr/src/app/export/exportProjet.page.tsx +++ b/packages/applications/ssr/src/app/export/exportProjet.page.tsx @@ -11,9 +11,17 @@ export const ExportProjetPage: FC = () => (
Cette page permet d'accèder à des liens pour exporter des données des projets
- +
+ + +
); diff --git "a/packages/applications/ssr/src/app/periodes/notifierP\303\251riode.action.ts" "b/packages/applications/ssr/src/app/periodes/notifierP\303\251riode.action.ts" index 74903a2d24..f26f8aa6c3 100644 --- "a/packages/applications/ssr/src/app/periodes/notifierP\303\251riode.action.ts" +++ "b/packages/applications/ssr/src/app/periodes/notifierP\303\251riode.action.ts" @@ -26,6 +26,7 @@ const action: FormAction = async (_, { appelOffre, per const candidatures = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { + utilisateur: utilisateur.identifiantUtilisateur.email, appelOffre, période: periode, estNotifiée: false, diff --git a/packages/applications/ssr/src/app/periodes/page.tsx b/packages/applications/ssr/src/app/periodes/page.tsx index cfde7ce7d1..12bf15e2ab 100644 --- a/packages/applications/ssr/src/app/periodes/page.tsx +++ b/packages/applications/ssr/src/app/periodes/page.tsx @@ -5,6 +5,7 @@ import { Période } from '@potentiel-domain/periode'; import { AppelOffre } from '@potentiel-domain/appel-offre'; import { Candidature } from '@potentiel-domain/projet'; import { Utilisateur } from '@potentiel-domain/utilisateur'; +import { PotentielUtilisateur } from '@potentiel-applications/request-context'; import { PageWithErrorHandling } from '@/utils/PageWithErrorHandling'; import { withUtilisateur } from '@/utils/withUtilisateur'; @@ -77,7 +78,9 @@ export default async function Page({ searchParams }: PageProps) { ]; const périodesPartiellementNotifiées = - estNotifiée === false ? await getPériodesPartiellementNotifiées(appelOffre) : []; + estNotifiée === false + ? await getPériodesPartiellementNotifiées(appelOffre, utilisateur) + : []; const props = await mapToProps({ utilisateur, @@ -113,6 +116,7 @@ const mapToProps: MapToProps = async ({ périodes, utilisateur }) => période.identifiantPériode.appelOffre, période.identifiantPériode.période, période.estNotifiée, + utilisateur, ); const props: PériodeListItemProps = { @@ -135,10 +139,12 @@ const getCandidaturesStatsForPeriode = async ( appelOffre: string, periode: string, estNotifiée: boolean, + utilisateur: PotentielUtilisateur, ) => { const candidatures = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { + utilisateur: utilisateur.identifiantUtilisateur.email, appelOffre, période: periode, }, @@ -161,10 +167,14 @@ const getCandidaturesStatsForPeriode = async ( /** * Périodes notifiées, avec au moins un candidat non notifié **/ -async function getPériodesPartiellementNotifiées(appelOffre: string | undefined) { +async function getPériodesPartiellementNotifiées( + appelOffre: string | undefined, + utilisateur: PotentielUtilisateur, +) { const candidats = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { + utilisateur: utilisateur.identifiantUtilisateur.email, estNotifiée: false, appelOffre, }, diff --git a/packages/domain/projet/src/candidature/lister/listerCandidatures.query.ts b/packages/domain/projet/src/candidature/lister/listerCandidatures.query.ts index c85c9e6c9f..469de7060f 100644 --- a/packages/domain/projet/src/candidature/lister/listerCandidatures.query.ts +++ b/packages/domain/projet/src/candidature/lister/listerCandidatures.query.ts @@ -6,8 +6,9 @@ import { Email } from '@potentiel-domain/common'; import { CandidatureEntity } from '../candidature.entity'; import { ConsulterCandidatureReadModel } from '../consulter/consulterCandidature.query'; import * as StatutCandidature from '../statutCandidature.valueType'; -import { DocumentProjet, IdentifiantProjet } from '../..'; +import { DocumentProjet, GetProjetUtilisateurScope, IdentifiantProjet } from '../..'; import { Dépôt, Localité, TypeActionnariat, UnitéPuissance } from '..'; +import { Fournisseur } from '../../lauréat'; export type CandidaturesListItemReadModel = { identifiantProjet: IdentifiantProjet.ValueType; @@ -24,6 +25,8 @@ export type CandidaturesListItemReadModel = { attestation?: DocumentProjet.ValueType; unitéPuissance: ConsulterCandidatureReadModel['unitéPuissance']; typeActionnariat?: Dépôt.ValueType['actionnariat']; + fournisseurs: Dépôt.ValueType['fournisseurs']; + sociétéMère: Dépôt.ValueType['sociétéMère']; }; export type ListerCandidaturesReadModel = Readonly<{ @@ -35,6 +38,7 @@ export type ListerCandidaturesReadModel = Readonly<{ export type ListerCandidaturesQuery = Message< 'Candidature.Query.ListerCandidatures', { + utilisateur: Email.RawType; range?: RangeOptions; statut?: StatutCandidature.RawType; appelOffre?: string; @@ -50,10 +54,15 @@ export type ListerCandidaturesQuery = Message< export type ListerCandidaturesQueryDependencies = { list: List; + getScopeProjetUtilisateur: GetProjetUtilisateurScope; }; -export const registerListerCandidaturesQuery = ({ list }: ListerCandidaturesQueryDependencies) => { +export const registerListerCandidaturesQuery = ({ + list, + getScopeProjetUtilisateur, +}: ListerCandidaturesQueryDependencies) => { const handler: MessageHandler = async ({ + utilisateur, range, statut, appelOffre, @@ -64,6 +73,8 @@ export const registerListerCandidaturesQuery = ({ list }: ListerCandidaturesQuer nomProjet, identifiantProjets, }) => { + const scope = await getScopeProjetUtilisateur(Email.convertirEnValueType(utilisateur)); + const { items, range: { endPosition, startPosition }, @@ -81,6 +92,10 @@ export const registerListerCandidaturesQuery = ({ list }: ListerCandidaturesQuer : undefined, nomProjet: Where.like(nomProjet), identifiantProjet: Where.matchAny(identifiantProjets), + + localité: { + région: scope.type === 'région' ? Where.matchAny(scope.régions) : undefined, + }, }, range, orderBy: { @@ -117,6 +132,8 @@ export const mapToReadModel = ({ notification, unitéPuissance, actionnariat, + fournisseurs, + sociétéMère, }: CandidatureEntity): CandidaturesListItemReadModel => ({ identifiantProjet: IdentifiantProjet.convertirEnValueType(identifiantProjet), statut: StatutCandidature.convertirEnValueType(statut), @@ -140,4 +157,6 @@ export const mapToReadModel = ({ }), unitéPuissance: UnitéPuissance.convertirEnValueType(unitéPuissance), typeActionnariat: actionnariat ? TypeActionnariat.convertirEnValueType(actionnariat) : undefined, + fournisseurs: fournisseurs.map(Fournisseur.Fournisseur.convertirEnValueType), + sociétéMère, }); diff --git a/packages/domain/utilisateur/src/role.valueType.ts b/packages/domain/utilisateur/src/role.valueType.ts index 90b4af55b7..de173784fa 100644 --- a/packages/domain/utilisateur/src/role.valueType.ts +++ b/packages/domain/utilisateur/src/role.valueType.ts @@ -1912,6 +1912,7 @@ const drealPolicies: ReadonlyArray = [ // Candidature 'candidature.attestation.télécharger', + 'candidature.lister', // Lauréat 'lauréat.modifierSiteDeProduction', @@ -2217,6 +2218,9 @@ const ademePolicies: ReadonlyArray = [ ...pageProjetPolicies, 'projet.accèsDonnées.prix', + + // Candidature + 'candidature.lister', ]; const policiesParRole: Record> = { diff --git "a/packages/specifications/src/p\303\251riode/stepDefinitions/p\303\251riode.then.ts" "b/packages/specifications/src/p\303\251riode/stepDefinitions/p\303\251riode.then.ts" index 4f3b8fc330..616fd02c91 100644 --- "a/packages/specifications/src/p\303\251riode/stepDefinitions/p\303\251riode.then.ts" +++ "b/packages/specifications/src/p\303\251riode/stepDefinitions/p\303\251riode.then.ts" @@ -11,6 +11,7 @@ import { Candidature } from '@potentiel-domain/projet'; import { Document } from '@potentiel-domain/projet'; import { Routes } from '@potentiel-applications/routes'; import { ListerUtilisateursQuery } from '@potentiel-domain/utilisateur'; +import { Email } from '@potentiel-domain/common'; import { PotentielWorld } from '../../potentiel.world'; import { convertReadableStreamToString } from '../../helpers/convertReadableToString'; @@ -41,6 +42,7 @@ Alors( const candidatures = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { + utilisateur: Email.système.formatter(), appelOffre: identifiantPériode.appelOffre, période: identifiantPériode.période, }, @@ -74,6 +76,7 @@ Alors( const candidatures = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { + utilisateur: Email.système.formatter(), appelOffre: identifiantPériode.appelOffre, période: identifiantPériode.période, }, @@ -89,6 +92,7 @@ Alors(`les porteurs doivent avoir accès à leur projet`, async function (this: const candidatures = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { + utilisateur: Email.système.formatter(), appelOffre: identifiantPériode.appelOffre, période: identifiantPériode.période, }, @@ -222,6 +226,7 @@ async function vérifierLauréats( const candidats = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { + utilisateur: Email.système.formatter(), appelOffre: identifiantPériode.appelOffre, période: identifiantPériode.période, statut: 'classé', @@ -268,6 +273,7 @@ async function vérifierÉliminés( const candidats = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { + utilisateur: Email.système.formatter(), appelOffre: identifiantPériode.appelOffre, période: identifiantPériode.période, statut: 'éliminé', From cf7788650a7dad4358e5d5e94f8a5b472e96e408 Mon Sep 17 00:00:00 2001 From: Hubert MONCENIS Date: Tue, 13 Jan 2026 16:43:29 +0100 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=9A=A7=20WIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/candidature/candidature.routes.ts | 2 +- .../ssr/src/app/export/export.page.tsx | 18 +++- .../ssr/src/app/export/exportProjet.page.tsx | 27 ------ .../applications/ssr/src/app/export/page.tsx | 4 + .../lister/listerFournisseurs.query.ts | 96 +++++++++++++++++++ .../domain/utilisateur/src/role.valueType.ts | 8 ++ 6 files changed, 126 insertions(+), 29 deletions(-) delete mode 100644 packages/applications/ssr/src/app/export/exportProjet.page.tsx create mode 100644 packages/domain/projet/src/candidature/lister/listerFournisseurs.query.ts diff --git a/packages/applications/routes/src/candidature/candidature.routes.ts b/packages/applications/routes/src/candidature/candidature.routes.ts index dd9a16251e..4515fdc649 100644 --- a/packages/applications/routes/src/candidature/candidature.routes.ts +++ b/packages/applications/routes/src/candidature/candidature.routes.ts @@ -57,4 +57,4 @@ export const prévisualiserAttestation = _avecIdentifiant('/previsualiser-attest // une fois qu'on aura migré la page Projet export const téléchargerAttestation = _avecIdentifiant('/telecharger-attestation'); -export const exporterFournisseurs = '/candidatures/export-fournisseurs'; +export const exporterFournisseur = '/candidatures/export-fournisseurs'; diff --git a/packages/applications/ssr/src/app/export/export.page.tsx b/packages/applications/ssr/src/app/export/export.page.tsx index d68263ac86..f733b5905f 100644 --- a/packages/applications/ssr/src/app/export/export.page.tsx +++ b/packages/applications/ssr/src/app/export/export.page.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import Notice from '@codegouvfr/react-dsfr/Notice'; import { Routes } from '@potentiel-applications/routes'; @@ -7,7 +8,7 @@ import { Heading1 } from '@/components/atoms/headings'; import { LinkAction } from '@/components/atoms/LinkAction'; export type ExportPageProps = { - actions: Array<'exporter-raccordement'>; + actions: Array<'exporter-raccordement' | 'exporter-fournisseur'>; }; export const ExportPage: FC = ({ actions }) => ( @@ -19,5 +20,20 @@ export const ExportPage: FC = ({ actions }) => ( {actions.includes('exporter-raccordement') && ( )} + + {actions.includes('exporter-fournisseur') && ( + <> + + + + )} ); diff --git a/packages/applications/ssr/src/app/export/exportProjet.page.tsx b/packages/applications/ssr/src/app/export/exportProjet.page.tsx deleted file mode 100644 index ab4d671e12..0000000000 --- a/packages/applications/ssr/src/app/export/exportProjet.page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { FC } from 'react'; - -import { Routes } from '@potentiel-applications/routes'; - -import { PageTemplate } from '@/components/templates/Page.template'; -import { Heading1 } from '@/components/atoms/headings'; -import { LinkAction } from '@/components/atoms/LinkAction'; - -export const ExportProjetPage: FC = () => ( - Exporter des données projets}> -
- Cette page permet d'accèder à des liens pour exporter des données des projets -
-
- - -
-
-); diff --git a/packages/applications/ssr/src/app/export/page.tsx b/packages/applications/ssr/src/app/export/page.tsx index 0f4d642627..65b173bc2d 100644 --- a/packages/applications/ssr/src/app/export/page.tsx +++ b/packages/applications/ssr/src/app/export/page.tsx @@ -27,5 +27,9 @@ const mapToAction: MapToAction = (utilisateur) => { return ['exporter-raccordement']; } + if (utilisateur.rôle.aLaPermission('candidature.exporterFournisseurs')) { + return ['exporter-fournisseur']; + } + return []; }; diff --git a/packages/domain/projet/src/candidature/lister/listerFournisseurs.query.ts b/packages/domain/projet/src/candidature/lister/listerFournisseurs.query.ts new file mode 100644 index 0000000000..c1632399ee --- /dev/null +++ b/packages/domain/projet/src/candidature/lister/listerFournisseurs.query.ts @@ -0,0 +1,96 @@ +import { Message, MessageHandler, mediator } from 'mediateur'; + +import { Joined, List, RangeOptions, Where } from '@potentiel-domain/entity'; +import { Email } from '@potentiel-domain/common'; + +import { CandidatureEntity } from '../candidature.entity'; +import { GetProjetUtilisateurScope, IdentifiantProjet } from '../..'; +import { Dépôt, DétailCandidature, DétailCandidatureEntity, Localité } from '..'; + +export type CandidatureFournisseurListItemReadModel = { + identifiantProjet: IdentifiantProjet.ValueType; + appelOffre: IdentifiantProjet.ValueType['appelOffre']; + période: IdentifiantProjet.ValueType['période']; + région: Localité.ValueType['région']; + sociétéMère: Dépôt.ValueType['sociétéMère']; + détail: DétailCandidature.RawType; +}; + +export type ListerCandidaturesReadModel = Readonly<{ + items: Array; + range: RangeOptions; + total: number; +}>; + +export type ListerFournisseursQuery = Message< + 'Candidature.Query.ListerFournisseurs', + { + utilisateur: Email.RawType; + }, + ListerCandidaturesReadModel +>; + +export type ListerFournisseursQueryDependencies = { + list: List; + getScopeProjetUtilisateur: GetProjetUtilisateurScope; +}; + +export const registerListerCandidaturesQuery = ({ + list, + getScopeProjetUtilisateur, +}: ListerFournisseursQueryDependencies) => { + const handler: MessageHandler = async ({ utilisateur }) => { + const scope = await getScopeProjetUtilisateur(Email.convertirEnValueType(utilisateur)); + + const { + items, + range: { endPosition, startPosition }, + total, + } = await list('candidature', { + join: { + entity: 'détail-candidature', + on: 'identifiantProjet', + }, + where: { + localité: { + région: scope.type === 'région' ? Where.matchAny(scope.régions) : undefined, + }, + }, + orderBy: { + appelOffre: 'ascending', + période: 'ascending', + nomProjet: 'ascending', + }, + }); + + return { + items: items.map(mapToReadModel), + range: { + endPosition, + startPosition, + }, + total, + }; + }; + mediator.register('Candidature.Query.ListerFournisseurs', handler); +}; + +type MapToReadModel = ( + candidature: CandidatureEntity & Joined, +) => CandidatureFournisseurListItemReadModel; + +export const mapToReadModel: MapToReadModel = ({ + identifiantProjet, + appelOffre, + période, + localité: { région }, + sociétéMère, + 'détail-candidature': { détail }, +}) => ({ + identifiantProjet: IdentifiantProjet.convertirEnValueType(identifiantProjet), + appelOffre, + période, + région, + sociétéMère, + détail, +}); diff --git a/packages/domain/utilisateur/src/role.valueType.ts b/packages/domain/utilisateur/src/role.valueType.ts index de173784fa..b86de6b39b 100644 --- a/packages/domain/utilisateur/src/role.valueType.ts +++ b/packages/domain/utilisateur/src/role.valueType.ts @@ -547,6 +547,7 @@ const référencielPermissions = { listerProjetsPreuveRecandidature: 'Candidature.Query.ListerProjetsEligiblesPreuveRecandidature', listerCandidatures: 'Candidature.Query.ListerCandidatures', + listerFournisseursÀLaCandidature: 'Candidature.Query.ListerFournisseurs', }, usecase: { importer: 'Candidature.UseCase.ImporterCandidature', @@ -998,6 +999,9 @@ const policies = { référencielPermissions.éliminé.recours.query.consulter, ], }, + exporterFournisseurs: [ + référencielPermissions.candidature.query.listerFournisseursÀLaCandidature, + ], }, période: { consulter: [référencielPermissions.période.query.consulter], @@ -1691,6 +1695,7 @@ const adminPolicies: ReadonlyArray = [ 'candidature.corriger', 'candidature.lister', 'candidature.attestation.prévisualiser', + 'candidature.exporterFournisseurs', // Période 'période.lister', @@ -1860,6 +1865,7 @@ const crePolicies: ReadonlyArray = [ // Candidature 'candidature.consulterDétail', 'candidature.consulter', + 'candidature.exporterFournisseurs', ]; const drealPolicies: ReadonlyArray = [ @@ -1913,6 +1919,7 @@ const drealPolicies: ReadonlyArray = [ // Candidature 'candidature.attestation.télécharger', 'candidature.lister', + 'candidature.exporterFournisseurs', // Lauréat 'lauréat.modifierSiteDeProduction', @@ -2221,6 +2228,7 @@ const ademePolicies: ReadonlyArray = [ // Candidature 'candidature.lister', + 'candidature.exporterFournisseurs', ]; const policiesParRole: Record> = { From 47b6e4a0481dd42c8913f602f6fadbaa85bf5724 Mon Sep 17 00:00:00 2001 From: Hubert MONCENIS Date: Wed, 14 Jan 2026 15:29:40 +0100 Subject: [PATCH 5/8] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20Lister=20candidatures?= =?UTF-8?q?=20sans=20utilisateur=20ajout=C3=A9=20+=20query=20d=C3=A9di?= =?UTF-8?q?=C3=A9e=20listerFournisseurs=C3=80LaCandidature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../load-test/notifier-candidatures.ts | 1 - ...idature-r\303\251f\303\251rentiel-dgec.ts" | 7 +- .../ssr/src/app/candidatures/(liste)/page.tsx | 204 +++++++++--------- .../notifierP\303\251riode.action.ts" | 1 - .../ssr/src/app/periodes/page.tsx | 14 +- .../lister/listerCandidatures.query.ts | 23 +- .../lister/listerFournisseurs.query.ts | 96 --------- ...ournisseurs\303\200LaCandidature.query.ts" | 160 ++++++++++++++ .../domain/utilisateur/src/role.valueType.ts | 13 -- .../stepDefinitions/p\303\251riode.then.ts" | 6 - 10 files changed, 265 insertions(+), 260 deletions(-) delete mode 100644 packages/domain/projet/src/candidature/lister/listerFournisseurs.query.ts create mode 100644 "packages/domain/projet/src/candidature/lister/listerFournisseurs\303\200LaCandidature.query.ts" diff --git a/packages/applications/cli/src/commands/load-test/notifier-candidatures.ts b/packages/applications/cli/src/commands/load-test/notifier-candidatures.ts index d901429edb..a823bf700c 100644 --- a/packages/applications/cli/src/commands/load-test/notifier-candidatures.ts +++ b/packages/applications/cli/src/commands/load-test/notifier-candidatures.ts @@ -71,7 +71,6 @@ export class NotifierCandidatures extends Command { const candidatures = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { - utilisateur: Email.système.formatter(), appelOffre, période: String(periode), estNotifiée: false, diff --git "a/packages/applications/scheduled-tasks/src/garanties-financi\303\250res/r\303\251cup\303\251rer-fichier-gf-candidature-r\303\251f\303\251rentiel-dgec.ts" "b/packages/applications/scheduled-tasks/src/garanties-financi\303\250res/r\303\251cup\303\251rer-fichier-gf-candidature-r\303\251f\303\251rentiel-dgec.ts" index b2da3ae879..458b83e9c8 100644 --- "a/packages/applications/scheduled-tasks/src/garanties-financi\303\250res/r\303\251cup\303\251rer-fichier-gf-candidature-r\303\251f\303\251rentiel-dgec.ts" +++ "b/packages/applications/scheduled-tasks/src/garanties-financi\303\250res/r\303\251cup\303\251rer-fichier-gf-candidature-r\303\251f\303\251rentiel-dgec.ts" @@ -9,11 +9,7 @@ import { Option } from '@potentiel-libraries/monads'; import { DateTime, Email } from '@potentiel-domain/common'; import { findProjection, listProjection } from '@potentiel-infrastructure/pg-projection-read'; import { Document, IdentifiantProjet, Candidature, Lauréat } from '@potentiel-domain/projet'; -import { - ProjetAdapter, - DocumentAdapter, - getProjetUtilisateurScopeAdapter, -} from '@potentiel-infrastructure/domain-adapters'; +import { ProjetAdapter, DocumentAdapter } from '@potentiel-infrastructure/domain-adapters'; import { Période } from '@potentiel-domain/periode'; export const dgecEmail = 'aopv.dgec@developpement-durable.gouv.fr'; @@ -42,7 +38,6 @@ Candidature.registerCandidatureQueries({ list: listProjection, récupérerProjetsEligiblesPreuveRecanditure: ProjetAdapter.récupérerProjetsEligiblesPreuveRecanditureAdapter, - getScopeProjetUtilisateur: getProjetUtilisateurScopeAdapter, }); Document.registerDocumentProjetCommand({ diff --git a/packages/applications/ssr/src/app/candidatures/(liste)/page.tsx b/packages/applications/ssr/src/app/candidatures/(liste)/page.tsx index 69213f98e1..5af25bc68e 100644 --- a/packages/applications/ssr/src/app/candidatures/(liste)/page.tsx +++ b/packages/applications/ssr/src/app/candidatures/(liste)/page.tsx @@ -14,7 +14,6 @@ import { ListFilterItem } from '@/components/molecules/ListFilters'; import { transformToOptionalEnumArray } from '@/app/_helpers/transformToOptionalStringArray'; import { getTypeActionnariatFilterOptions } from '@/app/_helpers/filters/getTypeActionnariatFilterOptions'; import { candidatureListLegendSymbols } from '@/components/molecules/candidature/CandidatureListLegendAndSymbols'; -import { withUtilisateur } from '@/utils/withUtilisateur'; import { CandidatureListPage } from './CandidatureList.page'; @@ -44,107 +43,104 @@ const paramsSchema = z.object({ type SearchParams = keyof z.infer; export default async function Page({ searchParams }: PageProps) { - return PageWithErrorHandling(async () => - withUtilisateur(async (utilisateur) => { - const { page, appelOffre, famille, nomProjet, periode, statut, notifie, typeActionnariat } = - paramsSchema.parse(searchParams); - - if (nomProjet && IdentifiantProjet.estValide(nomProjet)) { - return redirect(Routes.Candidature.détails(nomProjet)); - } - - const candidatures = await mediator.send({ - type: 'Candidature.Query.ListerCandidatures', - data: { - utilisateur: utilisateur.identifiantUtilisateur.email, - range: mapToRangeOptions({ - currentPage: page, - itemsPerPage: 10, - }), - nomProjet, - appelOffre, - période: periode, - famille, - statut, - typeActionnariat, - estNotifiée: notifie, - }, - }); - - const appelOffres = await mediator.send({ - type: 'AppelOffre.Query.ListerAppelOffre', - data: {}, - }); - - const appelOffresFiltré = appelOffres.items.find((a) => a.id === appelOffre); - - const périodeFiltrée = appelOffresFiltré?.periodes.find((p) => p.id === periode); - - const périodeOptions = - appelOffresFiltré?.periodes.map(({ title, id }) => ({ label: title, value: id })) ?? []; - - const familleOptions = - périodeFiltrée?.familles.map(({ title, id }) => ({ label: title, value: id })) ?? []; - - const filters: ListFilterItem[] = [ - { - label: 'Statut de la candidature', - searchParamKey: 'statut', - options: [ - { label: 'Classé', value: Candidature.StatutCandidature.classé.formatter() }, - { label: 'Éliminé', value: Candidature.StatutCandidature.éliminé.formatter() }, - ], - }, - { - label: 'Statut de la notification', - searchParamKey: 'notifie', - options: [ - { label: 'Notifié', value: 'notifie' }, - { label: 'À notifier', value: 'a-notifier' }, - ], - }, - { - label: `Appel d'offres`, - searchParamKey: 'appelOffre', - options: appelOffres.items.map((appelOffre) => ({ - label: appelOffre.id, - value: appelOffre.id, - })), - affects: ['periode', 'famille'], - }, - { - label: 'Période', - searchParamKey: 'periode', - options: périodeOptions, - affects: ['famille'], - }, - { - label: 'Famille', - searchParamKey: 'famille', - options: familleOptions, - }, - { - label: "Type d'actionnariat", - searchParamKey: 'typeActionnariat', - options: getTypeActionnariatFilterOptions(appelOffresFiltré?.cycleAppelOffre), - multiple: true, - }, - ]; - - return ( - - ); - }), - ); + return PageWithErrorHandling(async () => { + const { page, appelOffre, famille, nomProjet, periode, statut, notifie, typeActionnariat } = + paramsSchema.parse(searchParams); + + if (nomProjet && IdentifiantProjet.estValide(nomProjet)) { + return redirect(Routes.Candidature.détails(nomProjet)); + } + + const candidatures = await mediator.send({ + type: 'Candidature.Query.ListerCandidatures', + data: { + range: mapToRangeOptions({ + currentPage: page, + itemsPerPage: 10, + }), + nomProjet, + appelOffre, + période: periode, + famille, + statut, + typeActionnariat, + estNotifiée: notifie, + }, + }); + + const appelOffres = await mediator.send({ + type: 'AppelOffre.Query.ListerAppelOffre', + data: {}, + }); + + const appelOffresFiltré = appelOffres.items.find((a) => a.id === appelOffre); + + const périodeFiltrée = appelOffresFiltré?.periodes.find((p) => p.id === periode); + + const périodeOptions = + appelOffresFiltré?.periodes.map(({ title, id }) => ({ label: title, value: id })) ?? []; + + const familleOptions = + périodeFiltrée?.familles.map(({ title, id }) => ({ label: title, value: id })) ?? []; + + const filters: ListFilterItem[] = [ + { + label: 'Statut de la candidature', + searchParamKey: 'statut', + options: [ + { label: 'Classé', value: Candidature.StatutCandidature.classé.formatter() }, + { label: 'Éliminé', value: Candidature.StatutCandidature.éliminé.formatter() }, + ], + }, + { + label: 'Statut de la notification', + searchParamKey: 'notifie', + options: [ + { label: 'Notifié', value: 'notifie' }, + { label: 'À notifier', value: 'a-notifier' }, + ], + }, + { + label: `Appel d'offres`, + searchParamKey: 'appelOffre', + options: appelOffres.items.map((appelOffre) => ({ + label: appelOffre.id, + value: appelOffre.id, + })), + affects: ['periode', 'famille'], + }, + { + label: 'Période', + searchParamKey: 'periode', + options: périodeOptions, + affects: ['famille'], + }, + { + label: 'Famille', + searchParamKey: 'famille', + options: familleOptions, + }, + { + label: "Type d'actionnariat", + searchParamKey: 'typeActionnariat', + options: getTypeActionnariatFilterOptions(appelOffresFiltré?.cycleAppelOffre), + multiple: true, + }, + ]; + + return ( + + ); + }); } diff --git "a/packages/applications/ssr/src/app/periodes/notifierP\303\251riode.action.ts" "b/packages/applications/ssr/src/app/periodes/notifierP\303\251riode.action.ts" index f26f8aa6c3..74903a2d24 100644 --- "a/packages/applications/ssr/src/app/periodes/notifierP\303\251riode.action.ts" +++ "b/packages/applications/ssr/src/app/periodes/notifierP\303\251riode.action.ts" @@ -26,7 +26,6 @@ const action: FormAction = async (_, { appelOffre, per const candidatures = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { - utilisateur: utilisateur.identifiantUtilisateur.email, appelOffre, période: periode, estNotifiée: false, diff --git a/packages/applications/ssr/src/app/periodes/page.tsx b/packages/applications/ssr/src/app/periodes/page.tsx index 12bf15e2ab..cfde7ce7d1 100644 --- a/packages/applications/ssr/src/app/periodes/page.tsx +++ b/packages/applications/ssr/src/app/periodes/page.tsx @@ -5,7 +5,6 @@ import { Période } from '@potentiel-domain/periode'; import { AppelOffre } from '@potentiel-domain/appel-offre'; import { Candidature } from '@potentiel-domain/projet'; import { Utilisateur } from '@potentiel-domain/utilisateur'; -import { PotentielUtilisateur } from '@potentiel-applications/request-context'; import { PageWithErrorHandling } from '@/utils/PageWithErrorHandling'; import { withUtilisateur } from '@/utils/withUtilisateur'; @@ -78,9 +77,7 @@ export default async function Page({ searchParams }: PageProps) { ]; const périodesPartiellementNotifiées = - estNotifiée === false - ? await getPériodesPartiellementNotifiées(appelOffre, utilisateur) - : []; + estNotifiée === false ? await getPériodesPartiellementNotifiées(appelOffre) : []; const props = await mapToProps({ utilisateur, @@ -116,7 +113,6 @@ const mapToProps: MapToProps = async ({ périodes, utilisateur }) => période.identifiantPériode.appelOffre, période.identifiantPériode.période, période.estNotifiée, - utilisateur, ); const props: PériodeListItemProps = { @@ -139,12 +135,10 @@ const getCandidaturesStatsForPeriode = async ( appelOffre: string, periode: string, estNotifiée: boolean, - utilisateur: PotentielUtilisateur, ) => { const candidatures = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { - utilisateur: utilisateur.identifiantUtilisateur.email, appelOffre, période: periode, }, @@ -167,14 +161,10 @@ const getCandidaturesStatsForPeriode = async ( /** * Périodes notifiées, avec au moins un candidat non notifié **/ -async function getPériodesPartiellementNotifiées( - appelOffre: string | undefined, - utilisateur: PotentielUtilisateur, -) { +async function getPériodesPartiellementNotifiées(appelOffre: string | undefined) { const candidats = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { - utilisateur: utilisateur.identifiantUtilisateur.email, estNotifiée: false, appelOffre, }, diff --git a/packages/domain/projet/src/candidature/lister/listerCandidatures.query.ts b/packages/domain/projet/src/candidature/lister/listerCandidatures.query.ts index 469de7060f..c85c9e6c9f 100644 --- a/packages/domain/projet/src/candidature/lister/listerCandidatures.query.ts +++ b/packages/domain/projet/src/candidature/lister/listerCandidatures.query.ts @@ -6,9 +6,8 @@ import { Email } from '@potentiel-domain/common'; import { CandidatureEntity } from '../candidature.entity'; import { ConsulterCandidatureReadModel } from '../consulter/consulterCandidature.query'; import * as StatutCandidature from '../statutCandidature.valueType'; -import { DocumentProjet, GetProjetUtilisateurScope, IdentifiantProjet } from '../..'; +import { DocumentProjet, IdentifiantProjet } from '../..'; import { Dépôt, Localité, TypeActionnariat, UnitéPuissance } from '..'; -import { Fournisseur } from '../../lauréat'; export type CandidaturesListItemReadModel = { identifiantProjet: IdentifiantProjet.ValueType; @@ -25,8 +24,6 @@ export type CandidaturesListItemReadModel = { attestation?: DocumentProjet.ValueType; unitéPuissance: ConsulterCandidatureReadModel['unitéPuissance']; typeActionnariat?: Dépôt.ValueType['actionnariat']; - fournisseurs: Dépôt.ValueType['fournisseurs']; - sociétéMère: Dépôt.ValueType['sociétéMère']; }; export type ListerCandidaturesReadModel = Readonly<{ @@ -38,7 +35,6 @@ export type ListerCandidaturesReadModel = Readonly<{ export type ListerCandidaturesQuery = Message< 'Candidature.Query.ListerCandidatures', { - utilisateur: Email.RawType; range?: RangeOptions; statut?: StatutCandidature.RawType; appelOffre?: string; @@ -54,15 +50,10 @@ export type ListerCandidaturesQuery = Message< export type ListerCandidaturesQueryDependencies = { list: List; - getScopeProjetUtilisateur: GetProjetUtilisateurScope; }; -export const registerListerCandidaturesQuery = ({ - list, - getScopeProjetUtilisateur, -}: ListerCandidaturesQueryDependencies) => { +export const registerListerCandidaturesQuery = ({ list }: ListerCandidaturesQueryDependencies) => { const handler: MessageHandler = async ({ - utilisateur, range, statut, appelOffre, @@ -73,8 +64,6 @@ export const registerListerCandidaturesQuery = ({ nomProjet, identifiantProjets, }) => { - const scope = await getScopeProjetUtilisateur(Email.convertirEnValueType(utilisateur)); - const { items, range: { endPosition, startPosition }, @@ -92,10 +81,6 @@ export const registerListerCandidaturesQuery = ({ : undefined, nomProjet: Where.like(nomProjet), identifiantProjet: Where.matchAny(identifiantProjets), - - localité: { - région: scope.type === 'région' ? Where.matchAny(scope.régions) : undefined, - }, }, range, orderBy: { @@ -132,8 +117,6 @@ export const mapToReadModel = ({ notification, unitéPuissance, actionnariat, - fournisseurs, - sociétéMère, }: CandidatureEntity): CandidaturesListItemReadModel => ({ identifiantProjet: IdentifiantProjet.convertirEnValueType(identifiantProjet), statut: StatutCandidature.convertirEnValueType(statut), @@ -157,6 +140,4 @@ export const mapToReadModel = ({ }), unitéPuissance: UnitéPuissance.convertirEnValueType(unitéPuissance), typeActionnariat: actionnariat ? TypeActionnariat.convertirEnValueType(actionnariat) : undefined, - fournisseurs: fournisseurs.map(Fournisseur.Fournisseur.convertirEnValueType), - sociétéMère, }); diff --git a/packages/domain/projet/src/candidature/lister/listerFournisseurs.query.ts b/packages/domain/projet/src/candidature/lister/listerFournisseurs.query.ts deleted file mode 100644 index c1632399ee..0000000000 --- a/packages/domain/projet/src/candidature/lister/listerFournisseurs.query.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Message, MessageHandler, mediator } from 'mediateur'; - -import { Joined, List, RangeOptions, Where } from '@potentiel-domain/entity'; -import { Email } from '@potentiel-domain/common'; - -import { CandidatureEntity } from '../candidature.entity'; -import { GetProjetUtilisateurScope, IdentifiantProjet } from '../..'; -import { Dépôt, DétailCandidature, DétailCandidatureEntity, Localité } from '..'; - -export type CandidatureFournisseurListItemReadModel = { - identifiantProjet: IdentifiantProjet.ValueType; - appelOffre: IdentifiantProjet.ValueType['appelOffre']; - période: IdentifiantProjet.ValueType['période']; - région: Localité.ValueType['région']; - sociétéMère: Dépôt.ValueType['sociétéMère']; - détail: DétailCandidature.RawType; -}; - -export type ListerCandidaturesReadModel = Readonly<{ - items: Array; - range: RangeOptions; - total: number; -}>; - -export type ListerFournisseursQuery = Message< - 'Candidature.Query.ListerFournisseurs', - { - utilisateur: Email.RawType; - }, - ListerCandidaturesReadModel ->; - -export type ListerFournisseursQueryDependencies = { - list: List; - getScopeProjetUtilisateur: GetProjetUtilisateurScope; -}; - -export const registerListerCandidaturesQuery = ({ - list, - getScopeProjetUtilisateur, -}: ListerFournisseursQueryDependencies) => { - const handler: MessageHandler = async ({ utilisateur }) => { - const scope = await getScopeProjetUtilisateur(Email.convertirEnValueType(utilisateur)); - - const { - items, - range: { endPosition, startPosition }, - total, - } = await list('candidature', { - join: { - entity: 'détail-candidature', - on: 'identifiantProjet', - }, - where: { - localité: { - région: scope.type === 'région' ? Where.matchAny(scope.régions) : undefined, - }, - }, - orderBy: { - appelOffre: 'ascending', - période: 'ascending', - nomProjet: 'ascending', - }, - }); - - return { - items: items.map(mapToReadModel), - range: { - endPosition, - startPosition, - }, - total, - }; - }; - mediator.register('Candidature.Query.ListerFournisseurs', handler); -}; - -type MapToReadModel = ( - candidature: CandidatureEntity & Joined, -) => CandidatureFournisseurListItemReadModel; - -export const mapToReadModel: MapToReadModel = ({ - identifiantProjet, - appelOffre, - période, - localité: { région }, - sociétéMère, - 'détail-candidature': { détail }, -}) => ({ - identifiantProjet: IdentifiantProjet.convertirEnValueType(identifiantProjet), - appelOffre, - période, - région, - sociétéMère, - détail, -}); diff --git "a/packages/domain/projet/src/candidature/lister/listerFournisseurs\303\200LaCandidature.query.ts" "b/packages/domain/projet/src/candidature/lister/listerFournisseurs\303\200LaCandidature.query.ts" new file mode 100644 index 0000000000..c1ebe02fb1 --- /dev/null +++ "b/packages/domain/projet/src/candidature/lister/listerFournisseurs\303\200LaCandidature.query.ts" @@ -0,0 +1,160 @@ +import { Message, MessageHandler, mediator } from 'mediateur'; + +import { Joined, List, RangeOptions, Where } from '@potentiel-domain/entity'; +import { Email } from '@potentiel-domain/common'; + +import { CandidatureEntity } from '../candidature.entity'; +import { GetProjetUtilisateurScope, IdentifiantProjet } from '../..'; +import { Dépôt, DétailCandidature, DétailCandidatureEntity, Localité } from '..'; + +export type FournisseurÀLaCandidatureListItemReadModel = { + identifiantProjet: IdentifiantProjet.ValueType; + appelOffre: IdentifiantProjet.ValueType['appelOffre']; + période: IdentifiantProjet.ValueType['période']; + région: Localité.ValueType['région']; + sociétéMère: Dépôt.ValueType['sociétéMère']; + détail: DétailCandidature.RawType; +}; + +export type ListerFournisseursÀLaCandidatureReadModel = Readonly<{ + items: Array; + range: RangeOptions; + total: number; +}>; + +export type ListerFournisseursÀLaCandidatureQuery = Message< + 'Candidature.Query.ListerFournisseursÀLaCandidature', + { + utilisateur: Email.RawType; + }, + ListerFournisseursÀLaCandidatureReadModel +>; + +export type ListerFournisseursÀLaCandidatureQueryDependencies = { + list: List; + getScopeProjetUtilisateur: GetProjetUtilisateurScope; +}; + +export const registerListerCandidaturesQuery = ({ + list, + getScopeProjetUtilisateur, +}: ListerFournisseursÀLaCandidatureQueryDependencies) => { + const handler: MessageHandler = async ({ + utilisateur, + }) => { + const scope = await getScopeProjetUtilisateur(Email.convertirEnValueType(utilisateur)); + + const { + items, + range: { endPosition, startPosition }, + total, + } = await list('candidature', { + join: { + entity: 'détail-candidature', + on: 'identifiantProjet', + }, + where: { + localité: { + région: scope.type === 'région' ? Where.matchAny(scope.régions) : undefined, + }, + }, + orderBy: { + appelOffre: 'ascending', + période: 'ascending', + nomProjet: 'ascending', + }, + }); + + return { + items: items.map(mapToReadModel), + range: { + endPosition, + startPosition, + }, + total, + }; + }; + mediator.register('Candidature.Query.ListerFournisseursÀLaCandidature', handler); +}; + +type MapToReadModel = ( + candidature: CandidatureEntity & Joined, +) => FournisseurÀLaCandidatureListItemReadModel; + +export const mapToReadModel: MapToReadModel = ({ + identifiantProjet, + appelOffre, + période, + localité: { région }, + sociétéMère, + 'détail-candidature': { détail }, +}) => ({ + identifiantProjet: IdentifiantProjet.convertirEnValueType(identifiantProjet), + appelOffre, + période, + région, + sociétéMère, + détail: getFournisseursInfosFromDétail(détail), +}); + +const getFournisseursInfosFromDétail = (détail: DétailCandidature.RawType) => { + const targetKeys = [ + 'Nom du fabricant \n(Plaquettes de silicium (wafers))', + 'Lieu(x) de fabrication \n(Plaquettes de silicium (wafers))', + 'Coût total du lot (M€)\n(Plaquettes de silicium (wafers))', + 'Contenu local français (%)\n(Plaquettes de silicium (wafers))', + 'Contenu local européen (%)\n(Plaquettes de silicium (wafers))', + 'Nom du fabricant \n(Polysilicium)', + 'Lieu(x) de fabrication \n(Polysilicium)', + 'Coût total du lot (M€)\n(Polysilicium)', + 'Contenu local français (%)\n(Polysilicium)', + 'Contenu local européen (%)\n(Polysilicium)', + 'Nom du fabricant \n(Postes de conversion)', + 'Lieu(x) de fabrication \n(Postes de conversion)', + 'Coût total du lot (M€)\n(Postes de conversion)', + 'Contenu local français (%)\n(Postes de conversion)', + 'Contenu local européen (%)\n(Postes de conversion)', + 'Nom du fabricant \n(Structure)', + 'Lieu(x) de fabrication \n(Structure)', + 'Coût total du lot (M€)\n(Structure)', + 'Contenu local français (%)\n(Structure)', + 'Contenu local européen (%)\n(Structure)', + 'Technologie \n(Dispositifs de stockage de l’énergie *)', + 'Nom du fabricant \n(Dispositifs de stockage de l’énergie *)', + 'Lieu(x) de fabrication \n(Dispositifs de stockage de l’énergie *)', + 'Coût total du lot (M€)\n(Dispositifs de stockage de l’énergie *)', + 'Contenu local français (%)\n(Dispositifs de stockage de l’énergie *)', + 'Contenu local européen (%)\n(Dispositifs de stockage de l’énergie *)', + 'Technologie \n(Dispositifs de suivi de la course du soleil *)', + 'Nom du fabricant \n(Dispositifs de suivi de la course du soleil *)', + 'Lieu(x) de fabrication \n(Dispositifs de suivi de la course du soleil *)', + 'Coût total du lot (M€)\n(Dispositifs de suivi de la course du soleil *)', + 'Contenu local français (%)\n(Dispositifs de suivi de la course du soleil *)', + 'Contenu local européen (%)\n(Dispositifs de suivi de la course du soleil *)', + 'Référence commerciale \n(Autres technologies)', + 'Nom du fabricant \n(Autres technologies)', + 'Lieu(x) de fabrication \n(Autres technologies)', + 'Coût total du lot (M€)\n(Autres technologies)', + 'Contenu local français (%)\n(Autres technologies)', + 'Contenu local européen (%)\n(Autres technologies)', + 'Coût total du lot (M€)\n(Installation et mise en service )', + 'Contenu local français (%)\n(Installation et mise en service) ', + 'Contenu local européen (%)\n(Installation et mise en service)', + 'Commentaires contenu local\n(Installation et mise en service)', + 'Coût total du lot (M€)\n(raccordement)', + 'Contenu local français (%)\n(raccordement)', + 'Contenu local européen (%)\n(raccordement)', + 'Contenu local TOTAL :\ncoût total (M€)', + 'Contenu local TOTAL français (%)', + 'Contenu local TOTAL européen (%)', + 'Contenu local TOTAL :\nCommentaires', + ]; + + const filteredDetails: DétailCandidature.RawType = {}; + for (const key of targetKeys) { + if (key in détail) { + filteredDetails[key] = détail[key]; + } + } + return filteredDetails; +}; diff --git a/packages/domain/utilisateur/src/role.valueType.ts b/packages/domain/utilisateur/src/role.valueType.ts index b86de6b39b..7c5a8d8ef9 100644 --- a/packages/domain/utilisateur/src/role.valueType.ts +++ b/packages/domain/utilisateur/src/role.valueType.ts @@ -650,13 +650,6 @@ const référencielPermissions = { lister: 'Lauréat.Query.ListerHistoriqueProjet', }, }, - projet: { - export: { - query: { - exportRaccordement: 'Projet.Query.ExportRaccordement', - }, - }, - }, } as const; /** @@ -1546,9 +1539,6 @@ const policies = { accèsDonnées: { prix: [], }, - export: { - exportRaccordement: [référencielPermissions.projet.export.query.exportRaccordement], - }, }, appelOffre: { consulter: [référencielPermissions.appelOffre.query.consulter], @@ -1782,9 +1772,6 @@ const adminPolicies: ReadonlyArray = [ // Tâche 'tâche.consulter', - - // Export projet - 'projet.export.exportRaccordement', ]; const dgecValidateurPolicies: ReadonlyArray = [ diff --git "a/packages/specifications/src/p\303\251riode/stepDefinitions/p\303\251riode.then.ts" "b/packages/specifications/src/p\303\251riode/stepDefinitions/p\303\251riode.then.ts" index 616fd02c91..4f3b8fc330 100644 --- "a/packages/specifications/src/p\303\251riode/stepDefinitions/p\303\251riode.then.ts" +++ "b/packages/specifications/src/p\303\251riode/stepDefinitions/p\303\251riode.then.ts" @@ -11,7 +11,6 @@ import { Candidature } from '@potentiel-domain/projet'; import { Document } from '@potentiel-domain/projet'; import { Routes } from '@potentiel-applications/routes'; import { ListerUtilisateursQuery } from '@potentiel-domain/utilisateur'; -import { Email } from '@potentiel-domain/common'; import { PotentielWorld } from '../../potentiel.world'; import { convertReadableStreamToString } from '../../helpers/convertReadableToString'; @@ -42,7 +41,6 @@ Alors( const candidatures = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { - utilisateur: Email.système.formatter(), appelOffre: identifiantPériode.appelOffre, période: identifiantPériode.période, }, @@ -76,7 +74,6 @@ Alors( const candidatures = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { - utilisateur: Email.système.formatter(), appelOffre: identifiantPériode.appelOffre, période: identifiantPériode.période, }, @@ -92,7 +89,6 @@ Alors(`les porteurs doivent avoir accès à leur projet`, async function (this: const candidatures = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { - utilisateur: Email.système.formatter(), appelOffre: identifiantPériode.appelOffre, période: identifiantPériode.période, }, @@ -226,7 +222,6 @@ async function vérifierLauréats( const candidats = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { - utilisateur: Email.système.formatter(), appelOffre: identifiantPériode.appelOffre, période: identifiantPériode.période, statut: 'classé', @@ -273,7 +268,6 @@ async function vérifierÉliminés( const candidats = await mediator.send({ type: 'Candidature.Query.ListerCandidatures', data: { - utilisateur: Email.système.formatter(), appelOffre: identifiantPériode.appelOffre, période: identifiantPériode.période, statut: 'éliminé', From 65a1bf97b5a4ed20e332059eca6f90fd01cb125c Mon Sep 17 00:00:00 2001 From: Hubert MONCENIS Date: Wed, 14 Jan 2026 17:03:22 +0100 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=A8=20Exporter=20le=20csv=20avec=20le?= =?UTF-8?q?s=20d=C3=A9tails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...idature-r\303\251f\303\251rentiel-dgec.ts" | 7 +- .../candidatures/export-fournisseurs/route.ts | 87 +++++------ .../ssr/src/app/export/export.page.tsx | 59 +++++--- .../applications/ssr/src/app/export/page.tsx | 8 +- .../src/candidature/candidature.register.ts | 8 +- .../domain/projet/src/candidature/index.ts | 9 ++ ...ournisseurs\303\200LaCandidature.query.ts" | 137 +++++++++++------- .../domain/utilisateur/src/role.valueType.ts | 4 +- 8 files changed, 193 insertions(+), 126 deletions(-) diff --git "a/packages/applications/scheduled-tasks/src/garanties-financi\303\250res/r\303\251cup\303\251rer-fichier-gf-candidature-r\303\251f\303\251rentiel-dgec.ts" "b/packages/applications/scheduled-tasks/src/garanties-financi\303\250res/r\303\251cup\303\251rer-fichier-gf-candidature-r\303\251f\303\251rentiel-dgec.ts" index 458b83e9c8..b2da3ae879 100644 --- "a/packages/applications/scheduled-tasks/src/garanties-financi\303\250res/r\303\251cup\303\251rer-fichier-gf-candidature-r\303\251f\303\251rentiel-dgec.ts" +++ "b/packages/applications/scheduled-tasks/src/garanties-financi\303\250res/r\303\251cup\303\251rer-fichier-gf-candidature-r\303\251f\303\251rentiel-dgec.ts" @@ -9,7 +9,11 @@ import { Option } from '@potentiel-libraries/monads'; import { DateTime, Email } from '@potentiel-domain/common'; import { findProjection, listProjection } from '@potentiel-infrastructure/pg-projection-read'; import { Document, IdentifiantProjet, Candidature, Lauréat } from '@potentiel-domain/projet'; -import { ProjetAdapter, DocumentAdapter } from '@potentiel-infrastructure/domain-adapters'; +import { + ProjetAdapter, + DocumentAdapter, + getProjetUtilisateurScopeAdapter, +} from '@potentiel-infrastructure/domain-adapters'; import { Période } from '@potentiel-domain/periode'; export const dgecEmail = 'aopv.dgec@developpement-durable.gouv.fr'; @@ -38,6 +42,7 @@ Candidature.registerCandidatureQueries({ list: listProjection, récupérerProjetsEligiblesPreuveRecanditure: ProjetAdapter.récupérerProjetsEligiblesPreuveRecanditureAdapter, + getScopeProjetUtilisateur: getProjetUtilisateurScopeAdapter, }); Document.registerDocumentProjetCommand({ diff --git a/packages/applications/ssr/src/app/candidatures/export-fournisseurs/route.ts b/packages/applications/ssr/src/app/candidatures/export-fournisseurs/route.ts index 6857b05503..6b9c88ad0a 100644 --- a/packages/applications/ssr/src/app/candidatures/export-fournisseurs/route.ts +++ b/packages/applications/ssr/src/app/candidatures/export-fournisseurs/route.ts @@ -1,55 +1,60 @@ -import { Parser } from '@json2csv/plainjs'; import { mediator } from 'mediateur'; -import { Candidature } from '@potentiel-domain/projet'; +import { Candidature, IdentifiantProjet } from '@potentiel-domain/projet'; +import { ExportCSV } from '@potentiel-libraries/csv'; import { apiAction } from '@/utils/apiAction'; -import { typeFournisseurLabel } from '@/app/laureats/[identifiant]/fournisseur/changement/typeFournisseurLabel'; import { withUtilisateur } from '@/utils/withUtilisateur'; +type CandidatureFournisseurCSV = { + identifiantProjet: IdentifiantProjet.RawType; + appelOffre: IdentifiantProjet.ValueType['appelOffre']; + periode: IdentifiantProjet.ValueType['période']; + region: Candidature.Localité.ValueType['région']; + societeMere: string; +} & Record; + export const GET = async (_: Request) => apiAction(async () => withUtilisateur(async (utilisateur) => { - const candidatures = await mediator.send({ - type: 'Candidature.Query.ListerCandidatures', - data: { - utilisateur: utilisateur.identifiantUtilisateur.email, - }, + const fournisseursÀLaCandidature = + await mediator.send({ + type: 'Candidature.Query.ListerFournisseursÀLaCandidature', + data: { + utilisateur: utilisateur.identifiantUtilisateur.email, + }, + }); + + const fournisseurCandidatureFields = Candidature.fournisseursCandidatureDétailKeys.map( + (key) => ({ label: key.replace(/\n/g, ''), value: key }), + ); + + const csv = await ExportCSV.toCSV({ + fields: [ + { label: 'Identifiant projet', value: 'identifiantProjet' }, + { label: "Appel d'offre", value: 'appelOffre' }, + { label: 'Période', value: 'periode' }, + { label: 'Région', value: 'region' }, + { label: 'Société mère', value: 'societeMere' }, + ...fournisseurCandidatureFields, + ], + data: fournisseursÀLaCandidature.items.map((fournisseur) => ({ + identifiantProjet: fournisseur.identifiantProjet.formatter(), + appelOffre: fournisseur.identifiantProjet.appelOffre, + periode: fournisseur.identifiantProjet.période, + region: fournisseur.région, + societeMere: fournisseur.sociétéMère, + ...Object.entries(fournisseur.détail).reduce( + (acc: Record, [key, value]) => { + acc[key] = value; + return acc; + }, + {}, + ), + })), }); - const data = []; - - for (const candidature of candidatures.items) { - for (const fournisseur of candidature.fournisseurs) { - data.push({ - identifiantProjet: candidature.identifiantProjet.formatter(), - appelOffre: candidature.identifiantProjet.appelOffre, - periode: candidature.identifiantProjet.période, - region: candidature.localité.région, - societeMere: candidature.sociétéMère, - typeFournisseur: typeFournisseurLabel[fournisseur.typeFournisseur.typeFournisseur], - nomDuFabricant: fournisseur.nomDuFabricant, - lieuDeFabrication: fournisseur.lieuDeFabrication, - }); - } - } - - const fields = [ - 'identifiantProjet', - 'appelOffre', - 'periode', - 'region', - 'societeMere', - 'typeFournisseur', - 'nomDuFabricant', - 'lieuDeFabrication', - ]; - - const csvParser = new Parser({ fields, delimiter: ';', withBOM: true }); - - const csv = csvParser.parse(data); - - const fileName = `export_projet_fournisseurs.csv`; + const fileName = `export_candidature_fournisseurs.csv`; return new Response(csv, { headers: { diff --git a/packages/applications/ssr/src/app/export/export.page.tsx b/packages/applications/ssr/src/app/export/export.page.tsx index f733b5905f..78a1b8890a 100644 --- a/packages/applications/ssr/src/app/export/export.page.tsx +++ b/packages/applications/ssr/src/app/export/export.page.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import Notice from '@codegouvfr/react-dsfr/Notice'; +import Alert from '@codegouvfr/react-dsfr/Alert'; import { Routes } from '@potentiel-applications/routes'; @@ -13,27 +13,42 @@ export type ExportPageProps = { export const ExportPage: FC = ({ actions }) => ( Exporter des données projets} feature={'export'}> -
- Cette page permet d'accéder à des liens pour exporter des données projets -
- - {actions.includes('exporter-raccordement') && ( - - )} +
+ {actions.includes('exporter-raccordement') && ( + <> +
+ Cet export permet de récupérer les données liées au raccordement des projets dans un + fichier CSV. +
+ + + )} - {actions.includes('exporter-fournisseur') && ( - <> - - - - )} + {actions.includes('exporter-fournisseur') && ( +
+ +
+ Attention, cet export concerne uniquement les données fournisseurs importées à la + candidature du projet. Il ne tient pas compte des éventuelles modifications + apportées au cours de la vie du projet +
+ + + } + className="mb-4" + /> +
+ )} +
); diff --git a/packages/applications/ssr/src/app/export/page.tsx b/packages/applications/ssr/src/app/export/page.tsx index 65b173bc2d..84a0b3ead9 100644 --- a/packages/applications/ssr/src/app/export/page.tsx +++ b/packages/applications/ssr/src/app/export/page.tsx @@ -23,13 +23,15 @@ export default async function Page() { type MapToAction = (utilisateur: PotentielUtilisateur) => ExportPageProps['actions']; const mapToAction: MapToAction = (utilisateur) => { + const actions: ExportPageProps['actions'] = []; + if (utilisateur.rôle.aLaPermission('raccordement.listerDossierRaccordement')) { - return ['exporter-raccordement']; + actions.push('exporter-raccordement'); } if (utilisateur.rôle.aLaPermission('candidature.exporterFournisseurs')) { - return ['exporter-fournisseur']; + actions.push('exporter-fournisseur'); } - return []; + return actions; }; diff --git a/packages/domain/projet/src/candidature/candidature.register.ts b/packages/domain/projet/src/candidature/candidature.register.ts index b7b2be6c57..95b5857f5e 100644 --- a/packages/domain/projet/src/candidature/candidature.register.ts +++ b/packages/domain/projet/src/candidature/candidature.register.ts @@ -19,6 +19,10 @@ import { registerImporterCandidatureUseCase } from './importer/importerCandidatu import { registerNotifierCandidatureCommand } from './notifier/notifierCandidature.command'; import { registerNotifierCandidatureUseCase } from './notifier/notifierCandidature.usecase'; import { registerConsulterDétailCandidatureQuery } from './détail/consulter/consulterDétailCandidature.query'; +import { + ListerFournisseursÀLaCandidatureQueryDependencies, + registerListerFournisseursÀLaCandidatureQuery, +} from './lister/listerFournisseursÀLaCandidature.query'; export type CandiatureCommandDependencies = { getProjetAggregateRoot: GetProjetAggregateRoot; @@ -26,13 +30,15 @@ export type CandiatureCommandDependencies = { export type CandidatureQueryDependencies = ListerProjetsEligiblesPreuveRecanditureDependencies & ConsulterCandidatureDependencies & - ListerCandidaturesQueryDependencies; + ListerCandidaturesQueryDependencies & + ListerFournisseursÀLaCandidatureQueryDependencies; export const registerCandidatureQueries = (dependencies: CandidatureQueryDependencies) => { registerProjetsEligiblesPreuveRecanditureQuery(dependencies); registerConsulterCandidatureQuery(dependencies); registerConsulterDétailCandidatureQuery(dependencies); registerListerCandidaturesQuery(dependencies); + registerListerFournisseursÀLaCandidatureQuery(dependencies); }; export const registerCandidaturesUseCases = ({ diff --git a/packages/domain/projet/src/candidature/index.ts b/packages/domain/projet/src/candidature/index.ts index eb9cd7bdf8..786cf45fcc 100644 --- a/packages/domain/projet/src/candidature/index.ts +++ b/packages/domain/projet/src/candidature/index.ts @@ -22,6 +22,10 @@ import { ListerCandidaturesQuery, ListerCandidaturesReadModel, } from './lister/listerCandidatures.query'; +import { + ListerFournisseursÀLaCandidatureQuery, + ListerFournisseursÀLaCandidatureReadModel, +} from './lister/listerFournisseursÀLaCandidature.query'; import { ListerProjetsEligiblesPreuveRecanditureQuery, ListerProjetsEligiblesPreuveRecanditureReadModel, @@ -38,12 +42,14 @@ import { NotifierCandidatureUseCase } from './notifier/notifierCandidature.useca export type CandidatureQuery = | ListerCandidaturesQuery | ListerProjetsEligiblesPreuveRecanditureQuery + | ListerFournisseursÀLaCandidatureQuery | ConsulterCandidatureQuery | ConsulterDétailCandidatureQuery; export { ListerProjetsEligiblesPreuveRecanditureQuery, ListerCandidaturesQuery, + ListerFournisseursÀLaCandidatureQuery, ConsulterCandidatureQuery, ConsulterDétailCandidatureQuery, }; @@ -52,6 +58,7 @@ export { export { ListerProjetsEligiblesPreuveRecanditureReadModel, ListerCandidaturesReadModel, + ListerFournisseursÀLaCandidatureReadModel, ConsulterCandidatureReadModel, ConsulterDétailCandidatureReadModel, }; @@ -103,3 +110,5 @@ export * as DétailCandidature from './détail/détailCandidature.valueType'; // Type export * from './détail/détailCandidature.valueType'; + +export { fournisseursCandidatureDétailKeys } from './lister/listerFournisseursÀLaCandidature.query'; diff --git "a/packages/domain/projet/src/candidature/lister/listerFournisseurs\303\200LaCandidature.query.ts" "b/packages/domain/projet/src/candidature/lister/listerFournisseurs\303\200LaCandidature.query.ts" index c1ebe02fb1..0f8c6e8e50 100644 --- "a/packages/domain/projet/src/candidature/lister/listerFournisseurs\303\200LaCandidature.query.ts" +++ "b/packages/domain/projet/src/candidature/lister/listerFournisseurs\303\200LaCandidature.query.ts" @@ -13,7 +13,7 @@ export type FournisseurÀLaCandidatureListItemReadModel = { période: IdentifiantProjet.ValueType['période']; région: Localité.ValueType['région']; sociétéMère: Dépôt.ValueType['sociétéMère']; - détail: DétailCandidature.RawType; + détail: Partial>; }; export type ListerFournisseursÀLaCandidatureReadModel = Readonly<{ @@ -35,7 +35,7 @@ export type ListerFournisseursÀLaCandidatureQueryDependencies = { getScopeProjetUtilisateur: GetProjetUtilisateurScope; }; -export const registerListerCandidaturesQuery = ({ +export const registerListerFournisseursÀLaCandidatureQuery = ({ list, getScopeProjetUtilisateur, }: ListerFournisseursÀLaCandidatureQueryDependencies) => { @@ -97,61 +97,88 @@ export const mapToReadModel: MapToReadModel = ({ détail: getFournisseursInfosFromDétail(détail), }); -const getFournisseursInfosFromDétail = (détail: DétailCandidature.RawType) => { - const targetKeys = [ - 'Nom du fabricant \n(Plaquettes de silicium (wafers))', - 'Lieu(x) de fabrication \n(Plaquettes de silicium (wafers))', - 'Coût total du lot (M€)\n(Plaquettes de silicium (wafers))', - 'Contenu local français (%)\n(Plaquettes de silicium (wafers))', - 'Contenu local européen (%)\n(Plaquettes de silicium (wafers))', - 'Nom du fabricant \n(Polysilicium)', - 'Lieu(x) de fabrication \n(Polysilicium)', - 'Coût total du lot (M€)\n(Polysilicium)', - 'Contenu local français (%)\n(Polysilicium)', - 'Contenu local européen (%)\n(Polysilicium)', - 'Nom du fabricant \n(Postes de conversion)', - 'Lieu(x) de fabrication \n(Postes de conversion)', - 'Coût total du lot (M€)\n(Postes de conversion)', - 'Contenu local français (%)\n(Postes de conversion)', - 'Contenu local européen (%)\n(Postes de conversion)', - 'Nom du fabricant \n(Structure)', - 'Lieu(x) de fabrication \n(Structure)', - 'Coût total du lot (M€)\n(Structure)', - 'Contenu local français (%)\n(Structure)', - 'Contenu local européen (%)\n(Structure)', - 'Technologie \n(Dispositifs de stockage de l’énergie *)', - 'Nom du fabricant \n(Dispositifs de stockage de l’énergie *)', - 'Lieu(x) de fabrication \n(Dispositifs de stockage de l’énergie *)', - 'Coût total du lot (M€)\n(Dispositifs de stockage de l’énergie *)', - 'Contenu local français (%)\n(Dispositifs de stockage de l’énergie *)', - 'Contenu local européen (%)\n(Dispositifs de stockage de l’énergie *)', - 'Technologie \n(Dispositifs de suivi de la course du soleil *)', - 'Nom du fabricant \n(Dispositifs de suivi de la course du soleil *)', - 'Lieu(x) de fabrication \n(Dispositifs de suivi de la course du soleil *)', - 'Coût total du lot (M€)\n(Dispositifs de suivi de la course du soleil *)', - 'Contenu local français (%)\n(Dispositifs de suivi de la course du soleil *)', - 'Contenu local européen (%)\n(Dispositifs de suivi de la course du soleil *)', - 'Référence commerciale \n(Autres technologies)', - 'Nom du fabricant \n(Autres technologies)', - 'Lieu(x) de fabrication \n(Autres technologies)', - 'Coût total du lot (M€)\n(Autres technologies)', - 'Contenu local français (%)\n(Autres technologies)', - 'Contenu local européen (%)\n(Autres technologies)', - 'Coût total du lot (M€)\n(Installation et mise en service )', - 'Contenu local français (%)\n(Installation et mise en service) ', - 'Contenu local européen (%)\n(Installation et mise en service)', - 'Commentaires contenu local\n(Installation et mise en service)', - 'Coût total du lot (M€)\n(raccordement)', - 'Contenu local français (%)\n(raccordement)', - 'Contenu local européen (%)\n(raccordement)', - 'Contenu local TOTAL :\ncoût total (M€)', - 'Contenu local TOTAL français (%)', - 'Contenu local TOTAL européen (%)', - 'Contenu local TOTAL :\nCommentaires', - ]; +export const fournisseursCandidatureDétailKeys = [ + 'Coût total du lot (M€)\n(Développement)', + 'Contenu local français (%)\n(Développement)', + 'Contenu local européen (%)\n(Développement)', + 'Contenu local développement :\nCommentaires', + 'Contenu local Fabrication de composants et assemblage :\nTotal coût du lot (M€)', + 'Contenu local Fabrication de composants et assemblage :\nPourcentage de contenu local français (%)', + 'Contenu local Fabrication de composants et assemblage :\nPourcentage de contenu local européen (%)', + 'Contenu local Fabrication de composants et assemblage :\nCommentaires', + 'Technologie (Modules ou films)', + 'Référence commerciale \n(Modules ou films)', + 'Lieu(x) de fabrication \n(Modules ou films)', + 'Puissance crête (Wc) \n(Modules ou films)', + 'Rendement nominal \n(Modules ou films)', + 'Coût des modules €/Wc', + 'Diamètre du rotor (m)\n(AO éolien)', + 'Hauteur bout de pâle (m)\n(AO éolien)', + 'Coût total du lot (M€)\n(Modules ou films)', + 'Contenu local français (%)\n(Modules ou films)', + 'Contenu local européen (%)\n(Modules ou films)', + 'Nom du fabricant (Cellules)', + 'Lieu(x) de fabrication (Cellules)', + 'Coût total du lot (M€)\n(Cellules)', + 'Contenu local français (%)\n(Cellules)', + 'Contenu local européen (%)\n(Cellules)', + 'Nom du fabricant \n(Plaquettes de silicium (wafers))', + 'Lieu(x) de fabrication \n(Plaquettes de silicium (wafers))', + 'Coût total du lot (M€)\n(Plaquettes de silicium (wafers))', + 'Contenu local français (%)\n(Plaquettes de silicium (wafers))', + 'Contenu local européen (%)\n(Plaquettes de silicium (wafers))', + 'Nom du fabricant \n(Polysilicium)', + 'Lieu(x) de fabrication \n(Polysilicium)', + 'Coût total du lot (M€)\n(Polysilicium)', + 'Contenu local français (%)\n(Polysilicium)', + 'Contenu local européen (%)\n(Polysilicium)', + 'Nom du fabricant \n(Postes de conversion)', + 'Lieu(x) de fabrication \n(Postes de conversion)', + 'Coût total du lot (M€)\n(Postes de conversion)', + 'Contenu local français (%)\n(Postes de conversion)', + 'Contenu local européen (%)\n(Postes de conversion)', + 'Nom du fabricant \n(Structure)', + 'Lieu(x) de fabrication \n(Structure)', + 'Coût total du lot (M€)\n(Structure)', + 'Contenu local français (%)\n(Structure)', + 'Contenu local européen (%)\n(Structure)', + 'Technologie \n(Dispositifs de stockage de l’énergie *)', + 'Nom du fabricant \n(Dispositifs de stockage de l’énergie *)', + 'Lieu(x) de fabrication \n(Dispositifs de stockage de l’énergie *)', + 'Coût total du lot (M€)\n(Dispositifs de stockage de l’énergie *)', + 'Contenu local français (%)\n(Dispositifs de stockage de l’énergie *)', + 'Contenu local européen (%)\n(Dispositifs de stockage de l’énergie *)', + 'Technologie \n(Dispositifs de suivi de la course du soleil *)', + 'Nom du fabricant \n(Dispositifs de suivi de la course du soleil *)', + 'Lieu(x) de fabrication \n(Dispositifs de suivi de la course du soleil *)', + 'Coût total du lot (M€)\n(Dispositifs de suivi de la course du soleil *)', + 'Contenu local français (%)\n(Dispositifs de suivi de la course du soleil *)', + 'Contenu local européen (%)\n(Dispositifs de suivi de la course du soleil *)', + 'Référence commerciale \n(Autres technologies)', + 'Nom du fabricant \n(Autres technologies)', + 'Lieu(x) de fabrication \n(Autres technologies)', + 'Coût total du lot (M€)\n(Autres technologies)', + 'Contenu local français (%)\n(Autres technologies)', + 'Contenu local européen (%)\n(Autres technologies)', + 'Coût total du lot (M€)\n(Installation et mise en service )', + 'Contenu local français (%)\n(Installation et mise en service) ', + 'Contenu local européen (%)\n(Installation et mise en service)', + 'Commentaires contenu local\n(Installation et mise en service)', + 'Coût total du lot (M€)\n(raccordement)', + 'Contenu local français (%)\n(raccordement)', + 'Contenu local européen (%)\n(raccordement)', + 'Contenu local TOTAL :\ncoût total (M€)', + 'Contenu local TOTAL français (%)', + 'Contenu local TOTAL européen (%)', + 'Contenu local TOTAL :\nCommentaires', +] as const; + +type FournisseursDétailKeys = (typeof fournisseursCandidatureDétailKeys)[number]; +const getFournisseursInfosFromDétail = (détail: DétailCandidature.RawType) => { const filteredDetails: DétailCandidature.RawType = {}; - for (const key of targetKeys) { + + for (const key of fournisseursCandidatureDétailKeys) { if (key in détail) { filteredDetails[key] = détail[key]; } diff --git a/packages/domain/utilisateur/src/role.valueType.ts b/packages/domain/utilisateur/src/role.valueType.ts index 7c5a8d8ef9..545822f4e4 100644 --- a/packages/domain/utilisateur/src/role.valueType.ts +++ b/packages/domain/utilisateur/src/role.valueType.ts @@ -547,7 +547,7 @@ const référencielPermissions = { listerProjetsPreuveRecandidature: 'Candidature.Query.ListerProjetsEligiblesPreuveRecandidature', listerCandidatures: 'Candidature.Query.ListerCandidatures', - listerFournisseursÀLaCandidature: 'Candidature.Query.ListerFournisseurs', + listerFournisseursÀLaCandidature: 'Candidature.Query.ListerFournisseursÀLaCandidature', }, usecase: { importer: 'Candidature.UseCase.ImporterCandidature', @@ -1905,7 +1905,6 @@ const drealPolicies: ReadonlyArray = [ // Candidature 'candidature.attestation.télécharger', - 'candidature.lister', 'candidature.exporterFournisseurs', // Lauréat @@ -2214,7 +2213,6 @@ const ademePolicies: ReadonlyArray = [ 'projet.accèsDonnées.prix', // Candidature - 'candidature.lister', 'candidature.exporterFournisseurs', ]; From 96be235a691d30699db06c1d80b323a545bf6259 Mon Sep 17 00:00:00 2001 From: Hubert Moncenis Date: Fri, 16 Jan 2026 09:28:53 +0100 Subject: [PATCH 7/8] Update packages/applications/ssr/src/app/export/export.page.tsx Co-authored-by: HanaeY <72154904+HanaeY@users.noreply.github.com> --- .../ssr/src/app/export/export.page.tsx | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/applications/ssr/src/app/export/export.page.tsx b/packages/applications/ssr/src/app/export/export.page.tsx index 78a1b8890a..45a1c2e274 100644 --- a/packages/applications/ssr/src/app/export/export.page.tsx +++ b/packages/applications/ssr/src/app/export/export.page.tsx @@ -19,7 +19,37 @@ export const ExportPage: FC = ({ actions }) => (
Cet export permet de récupérer les données liées au raccordement des projets dans un fichier CSV. -
+
+ {actions.includes('exporter-raccordement') && ( + + )} + + {actions.includes('exporter-fournisseur') && ( +
+ + + + Attention, cet export concerne uniquement les données fournisseurs importées à la + candidature des projets. Il ne tient pas compte des éventuelles modifications + apportées au cours de la vie des projets. + + + } + /> +
+ )} +
Date: Fri, 16 Jan 2026 09:34:37 +0100 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20layout=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ssr/src/app/export/export.page.tsx | 38 +------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/packages/applications/ssr/src/app/export/export.page.tsx b/packages/applications/ssr/src/app/export/export.page.tsx index 45a1c2e274..4f2a8bd426 100644 --- a/packages/applications/ssr/src/app/export/export.page.tsx +++ b/packages/applications/ssr/src/app/export/export.page.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import Alert from '@codegouvfr/react-dsfr/Alert'; +import Notice from '@codegouvfr/react-dsfr/Notice'; import { Routes } from '@potentiel-applications/routes'; @@ -14,12 +14,6 @@ export type ExportPageProps = { export const ExportPage: FC = ({ actions }) => ( Exporter des données projets} feature={'export'}>
- {actions.includes('exporter-raccordement') && ( - <> -
- Cet export permet de récupérer les données liées au raccordement des projets dans un - fichier CSV. -
{actions.includes('exporter-raccordement') && ( = ({ actions }) => ( />
)} -
- - - )} - - {actions.includes('exporter-fournisseur') && ( -
- -
- Attention, cet export concerne uniquement les données fournisseurs importées à la - candidature du projet. Il ne tient pas compte des éventuelles modifications - apportées au cours de la vie du projet -
- - - } - className="mb-4" - /> -
- )}
);