Skip to content

Commit

Permalink
feat: add declarations page
Browse files Browse the repository at this point in the history
  • Loading branch information
arturlg committed Dec 18, 2023
1 parent 5f02cd5 commit 96fd5ba
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 17 deletions.
47 changes: 47 additions & 0 deletions packages/app/src/app/(default)/mon-espace/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { declarationRepo, representationEquilibreeRepo } from "@api/core-domain/repo";
import { GetAllDeclarationsBySiren } from "@api/core-domain/useCases/GetAllDeclarationsBySiren";
import { GetDeclarationOpmcBySirenAndYear } from "@api/core-domain/useCases/GetDeclarationOpmcBySirenAndYear";
import { GetRepresentationEquilibreeBySiren } from "@api/core-domain/useCases/GetRepresentationEquilibreeBySiren";
import { assertServerSession } from "@api/utils/auth";

export async function getAllDeclarationsBySiren(siren: string) {
await assertServerSession({
owner: {
check: siren,
message: "Not authorized to fetch declarations for this siren.",
},
staff: true,
});

// handle default errors
const useCase = new GetAllDeclarationsBySiren(declarationRepo);
return await useCase.execute({ siren });
}

export async function getAllRepresentationEquilibreeBySiren(siren: string) {
await assertServerSession({
owner: {
check: siren,
message: "Not authorized to fetch representation equilibree for this siren.",
},
staff: true,
});

// handle default errors
const useCase = new GetRepresentationEquilibreeBySiren(representationEquilibreeRepo);
return await useCase.execute({ siren });
}

export async function getAllDeclarationOpmcSirenAndYear(siren: string, year: number) {
await assertServerSession({
owner: {
check: siren,
message: "Not authorized to fetch representation equilibree for this siren.",
},
staff: true,
});

// handle default errors
const useCase = new GetDeclarationOpmcBySirenAndYear(declarationRepo);
return await useCase.execute({ siren, year });
}
4 changes: 2 additions & 2 deletions packages/app/src/app/(default)/mon-espace/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { fr } from "@codegouvfr/react-dsfr";
import { Box, CenteredContainer } from "@design-system";
import { Box, Container } from "@design-system";
import { type PropsWithChildren } from "react";

const DEFAULT_TITLE = "Mon Espace";

const MonEspaceLayout = ({ children }: PropsWithChildren) => {
return (
<Box className={fr.cx("fr-mt-8w")}>
<CenteredContainer>{children}</CenteredContainer>
<Container>{children}</Container>
</Box>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,134 @@
export const IndexList = () => {
"use client";

import Table from "@codegouvfr/react-dsfr/Table";
import { CompanyWorkforceRange } from "@common/core-domain/domain/valueObjects/declaration/CompanyWorkforceRange";
import { type DeclarationDTO } from "@common/core-domain/dtos/DeclarationDTO";
import { type DeclarationOpmcDTO } from "@common/core-domain/dtos/DeclarationOpmcDTO";
import { formatIsoToFr } from "@common/utils/date";
import { Heading, Link } from "@design-system";
import { isBefore, sub } from "date-fns";
import { capitalize, upperCase } from "lodash";

import { buildHelpersObjectifsMesures } from "../../index-egapro/objectifs-mesures/[siren]/[year]/ObjectifsMesuresForm";

//Note: For 2022, first year of OPMC, we consider that the duration to be frozen is 2 years, but for next years, it will be 1 year like isFrozenDeclaration.
const OPMC_FROZEN_DURATION = { years: 2 };

const isFrozenDeclarationForOPMC = (declaration: DeclarationOpmcDTO) =>
declaration?.["declaration-existante"]?.date
? isBefore(new Date(declaration?.["declaration-existante"]?.date), sub(new Date(), OPMC_FROZEN_DURATION))
: false;

enum declarationOpmcStatus {
ALREADY_FILLED = "Déjà renseignés",
COMPLETED = "Renseignés",
INDEX_OVER_85 = "Index supérieur à 85",
NOT_APPLICABLE = "Non applicable",
NOT_MODIFIABLE = "Déclaration non modifiable",
NOT_MODIFIABLE_CORRECT = "Déclaration non modifiable sur données correctes",
NOT_MODIFIABLE_INCORRECT = "Déclaration non modifiable sur données incorrectes",
TO_COMPLETE = "À renseigner",
YEAR_NOT_APPLICABLE = "Année non applicable",
}

const getDeclarationOpmcStatus = (declaration?: DeclarationOpmcDTO) => {
if (!declaration) return declarationOpmcStatus.NOT_APPLICABLE;
const { after2021, index, initialValuesObjectifsMesures, objectifsMesuresSchema } =
buildHelpersObjectifsMesures(declaration);

if (!declaration["resultat-global"] || index === undefined) return declarationOpmcStatus.NOT_APPLICABLE;
if (!after2021) return declarationOpmcStatus.YEAR_NOT_APPLICABLE;
if (index >= 85) return declarationOpmcStatus.INDEX_OVER_85;

try {
objectifsMesuresSchema.parse(initialValuesObjectifsMesures);
if (isFrozenDeclarationForOPMC(declaration)) return declarationOpmcStatus.NOT_MODIFIABLE_CORRECT;
} catch (e) {
if (isFrozenDeclarationForOPMC(declaration)) return declarationOpmcStatus.NOT_MODIFIABLE_INCORRECT;
return declarationOpmcStatus.TO_COMPLETE;
}
return declarationOpmcStatus.COMPLETED;
};

const formatDeclarationOpmcStatus = (status: declarationOpmcStatus, siren: string, year: number) => {
const withLink = (text: string) => (
<Link key={`${siren}-objectifs-mesures`} href={`/index-egapro/objectifs-mesures/${siren}/${year}`}>
{text}
</Link>
);

switch (status) {
case declarationOpmcStatus.COMPLETED:
return withLink(declarationOpmcStatus.COMPLETED);
case declarationOpmcStatus.INDEX_OVER_85:
return declarationOpmcStatus.INDEX_OVER_85;
case declarationOpmcStatus.NOT_APPLICABLE:
return declarationOpmcStatus.NOT_APPLICABLE;
case declarationOpmcStatus.NOT_MODIFIABLE_CORRECT:
return withLink(declarationOpmcStatus.ALREADY_FILLED);
case declarationOpmcStatus.NOT_MODIFIABLE_INCORRECT:
return withLink(declarationOpmcStatus.NOT_MODIFIABLE);
case declarationOpmcStatus.TO_COMPLETE:
return withLink(declarationOpmcStatus.TO_COMPLETE);
case declarationOpmcStatus.YEAR_NOT_APPLICABLE:
return declarationOpmcStatus.YEAR_NOT_APPLICABLE;
default:
return declarationOpmcStatus.NOT_APPLICABLE;
}
};

export const IndexList = ({
declarations,
declarationOpmcList,
}: {
declarationOpmcList: DeclarationOpmcDTO[];
declarations: DeclarationDTO[];
}) => {
const headers = [
"SIREN",
"ANNÉE INDICATEUR",
"STRUCTURE",
"TRANCHE D'EFFECTIF",
"DATE DE DÉCLARATION",
"INDEX",
"OBJECTIF ET MESURES",
"RÉCAPITULATIF",
];

const data = declarations.map(declaration => {
const rowYear = declaration.commencer?.annéeIndicateurs;
const rowSiren = declaration.commencer?.siren;
return [
<Link key={declaration.commencer?.siren} href={`/index-egapro/declaration/${rowSiren}/${rowYear}`}>
{declaration.commencer?.siren}
</Link>,
rowYear,
declaration.entreprise?.type === "ues"
? upperCase(declaration.entreprise?.type)
: capitalize(declaration.entreprise?.type),
declaration.entreprise?.tranche ? CompanyWorkforceRange.Label[declaration.entreprise.tranche] : undefined,
formatIsoToFr(declaration["declaration-existante"].date || ""),
declaration["resultat-global"]?.index || <span title="Non calculable">NC</span>,
formatDeclarationOpmcStatus(
getDeclarationOpmcStatus(
declarationOpmcList.find(declarationOpmc => declarationOpmc.commencer?.annéeIndicateurs === rowYear),
),
rowSiren || "",
rowYear || 0,
),
<Link
key={`${rowSiren}-pdf`}
href={`/index-egapro/declaration/${rowSiren}/${rowYear}/pdf`}
download={`declaration_egapro_${rowSiren}_${Number(rowYear) + 1}.pdf`}
>
Télécharger
</Link>,
];
});
return (
<div>
<h1>IndexList</h1>
<Heading as="h1" variant="h5" text="Liste des déclarations transmises - Index Égalité Professionnelle" />
<Table headers={headers} data={data}></Table>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Table from "@codegouvfr/react-dsfr/Table";
import { type RepresentationEquilibreeDTO } from "@common/core-domain/dtos/RepresentationEquilibreeDTO";
import { formatIsoToFr } from "@common/utils/date";
import { Heading, Link } from "@design-system";

export const RepeqList = ({
representationEquilibrees,
}: {
representationEquilibrees: RepresentationEquilibreeDTO[];
}) => {
const headers = [
"SIREN",
"ANNÉE ÉCARTS",
"DATE DE DÉCLARATION",
"% FEMMES CADRES",
"% HOMMES CADRES",
"% FEMMES MEMBRES",
"% HOMMES MEMBRES",
"RÉCAPITULATIF",
];

function getPercent(
filterReasonKey: string,
percentKey: string,
representationEquilibree: RepresentationEquilibreeDTO,
) {
return (
(!(filterReasonKey in representationEquilibree) &&
representationEquilibree[percentKey as keyof RepresentationEquilibreeDTO]?.toString()) ||
"NC"
);
}

const data = representationEquilibrees.map(representationEquilibree => [
<Link
key={representationEquilibree.siren}
href={`/representation-equilibree/${representationEquilibree.siren}/${representationEquilibree.year}`}
>
{representationEquilibree.siren}
</Link>,
representationEquilibree.year,
formatIsoToFr(representationEquilibree.declaredAt),
getPercent("notComputableReasonExecutives", "executiveWomenPercent", representationEquilibree),
getPercent("notComputableReasonExecutives", "executiveMenPercent", representationEquilibree),
getPercent("notComputableReasonMembers", "memberWomenPercent", representationEquilibree),
getPercent("notComputableReasonMembers", "memberMenPercent", representationEquilibree),
<Link
key={`${representationEquilibree.siren}-pdf`}
href={`/representation-equilibree/${representationEquilibree.siren}/${representationEquilibree.year}/pdf`}
download={`declaration_egapro_${representationEquilibree.siren}_${Number(representationEquilibree.year) + 1}.pdf`}
>
Télécharger
</Link>,
]);
return (
<div>
<Heading as="h1" variant="h5" text="Liste des déclarations transmises - Représentation Équilibrée" />
<Table headers={headers} data={data}></Table>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use client";

import { fr } from "@codegouvfr/react-dsfr";
import SelectNext from "@codegouvfr/react-dsfr/SelectNext";
import { Grid, GridCol } from "@design-system";
import { zodResolver } from "@hookform/resolvers/zod";
import { first } from "lodash";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

export const SelectSiren = ({
sirenListWithCompanyName,
currentSiren,
}: {
currentSiren?: string;
sirenListWithCompanyName: Array<{ companyName: string; siren: string }>;
}) => {
const router = useRouter();
const sirenSchema = z.object({
siren: z
.string()
.length(9)
.regex(/^\d+$/)
.refine(siren => sirenList.includes(siren), {
message: "Vous n'avez pas les droits sur ce Siren.",
}),
});

type sirenFormType = z.infer<typeof sirenSchema>;

const {
register,
handleSubmit,
watch,
formState: { isValid },
} = useForm<sirenFormType>({
resolver: zodResolver(sirenSchema),
});

const sirenValue = watch("siren");
const sirenList = sirenListWithCompanyName.map(({ siren }) => siren);

const onSubmit = (data: sirenFormType) => {
router.push(`/mon-espace/mes-declarations?siren=${data.siren}`);
};

useEffect(() => {
if (isValid && sirenValue !== currentSiren) {
handleSubmit(onSubmit)();
}
}, [sirenValue, isValid, handleSubmit, onSubmit]);

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid>
<GridCol sm={3}>
<SelectNext
label="SIREN"
nativeSelectProps={{
...register("siren"),
}}
options={sirenList.map(value => ({
value,
label: value,
selected: currentSiren ? value === currentSiren : value === first(sirenList),
}))}
/>
</GridCol>
<GridCol sm={9}>
<div className={fr.cx("fr-pt-10v", "fr-pl-2v")}>
<span className={fr.cx("fr-icon-building-line", "fr-pr-2v")} aria-hidden="true"></span>
<span>{sirenListWithCompanyName.find(data => data.siren === sirenValue)?.companyName || ""}</span>
</div>
</GridCol>
</Grid>
</form>
);
};
Loading

0 comments on commit 96fd5ba

Please sign in to comment.