diff --git a/packages/applications/routes/src/candidature/candidature.routes.ts b/packages/applications/routes/src/candidature/candidature.routes.ts index fc951b4e7e6..4515fdc649d 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 exporterFournisseur = '/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 458b83e9c81..b2da3ae8797 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 new file mode 100644 index 00000000000..6b9c88ad0af --- /dev/null +++ b/packages/applications/ssr/src/app/candidatures/export-fournisseurs/route.ts @@ -0,0 +1,66 @@ +import { mediator } from 'mediateur'; + +import { Candidature, IdentifiantProjet } from '@potentiel-domain/projet'; +import { ExportCSV } from '@potentiel-libraries/csv'; + +import { apiAction } from '@/utils/apiAction'; +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 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 fileName = `export_candidature_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/export.page.tsx b/packages/applications/ssr/src/app/export/export.page.tsx index d68263ac86f..4f2a8bd4264 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,17 +8,41 @@ 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 }) => ( 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') && ( - - )} + {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. + + + } + /> +
+ )} +
); diff --git a/packages/applications/ssr/src/app/export/page.tsx b/packages/applications/ssr/src/app/export/page.tsx index 0f4d6426276..84a0b3ead94 100644 --- a/packages/applications/ssr/src/app/export/page.tsx +++ b/packages/applications/ssr/src/app/export/page.tsx @@ -23,9 +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')) { + 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 b7b2be6c573..95b5857f5ee 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 eb9cd7bdf84..786cf45fccd 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" new file mode 100644 index 00000000000..0f8c6e8e50e --- /dev/null +++ "b/packages/domain/projet/src/candidature/lister/listerFournisseurs\303\200LaCandidature.query.ts" @@ -0,0 +1,187 @@ +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: Partial>; +}; + +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 registerListerFournisseursÀLaCandidatureQuery = ({ + 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), +}); + +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 fournisseursCandidatureDétailKeys) { + 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 5bd1eb5449c..545822f4e47 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ÀLaCandidature', }, usecase: { importer: 'Candidature.UseCase.ImporterCandidature', @@ -991,6 +992,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], @@ -1681,6 +1685,7 @@ const adminPolicies: ReadonlyArray = [ 'candidature.corriger', 'candidature.lister', 'candidature.attestation.prévisualiser', + 'candidature.exporterFournisseurs', // Période 'période.lister', @@ -1847,6 +1852,7 @@ const crePolicies: ReadonlyArray = [ // Candidature 'candidature.consulterDétail', 'candidature.consulter', + 'candidature.exporterFournisseurs', ]; const drealPolicies: ReadonlyArray = [ @@ -1899,6 +1905,7 @@ const drealPolicies: ReadonlyArray = [ // Candidature 'candidature.attestation.télécharger', + 'candidature.exporterFournisseurs', // Lauréat 'lauréat.modifierSiteDeProduction', @@ -2204,6 +2211,9 @@ const ademePolicies: ReadonlyArray = [ ...pageProjetPolicies, 'projet.accèsDonnées.prix', + + // Candidature + 'candidature.exporterFournisseurs', ]; const policiesParRole: Record> = {