Skip to content

Commit b01856f

Browse files
committed
feat(api,admin): added visible boolean to certification, added Badge to certifcation based on status and visibility
1 parent 45344d0 commit b01856f

File tree

8 files changed

+215
-26
lines changed

8 files changed

+215
-26
lines changed

packages/reva-admin-react/src/app/(admin)/certifications-v2/(list)/layout.tsx

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
"use client";
22
import { Button } from "@codegouvfr/react-dsfr/Button";
3-
import SideMenu from "@codegouvfr/react-dsfr/SideMenu";
3+
import SideMenu, { SideMenuProps } from "@codegouvfr/react-dsfr/SideMenu";
44
import { useSearchParams } from "next/navigation";
55
import { ReactNode } from "react";
66

77
const CertificationListLayout = ({ children }: { children: ReactNode }) => {
88
const searchParams = useSearchParams();
9-
const statusParam = searchParams.get("status");
9+
const statusParam = searchParams.get("status") || undefined;
1010
const searchFilter = searchParams.get("search") || "";
11+
const visibleParam = searchParams.get("visible") || undefined;
1112

12-
const hrefSideMenu = (status: string | null) => {
13+
const hrefSideMenu = (status?: string, visible?: "true" | "false") => {
1314
const params = new URLSearchParams();
1415
if (status) {
1516
params.set("status", status);
1617
}
1718

19+
if (visible) {
20+
params.set("visible", visible);
21+
}
22+
1823
params.set("page", "1");
1924

2025
if (searchFilter) {
@@ -24,15 +29,32 @@ const CertificationListLayout = ({ children }: { children: ReactNode }) => {
2429
return `/certifications-v2/?${params.toString()}`;
2530
};
2631

27-
const menuItem = (text: string, status: string | null) => ({
28-
isActive: status === statusParam,
32+
const menuItem = (
33+
text: string,
34+
status?: string,
35+
visible?: "true" | "false",
36+
): SideMenuProps.Item => ({
37+
isActive: status === statusParam && visible === visibleParam,
2938
linkProps: {
30-
href: hrefSideMenu(status),
39+
href: hrefSideMenu(status, visible),
3140
target: "_self",
3241
},
3342
text,
3443
});
3544

45+
const items: SideMenuProps.Item[] = [
46+
menuItem("Toutes les certifications"),
47+
{
48+
...menuItem("Publiées", "VALIDE_PAR_CERTIFICATEUR"),
49+
items: [
50+
menuItem("Visibles", "VALIDE_PAR_CERTIFICATEUR", "true"),
51+
menuItem("Invisibles", "VALIDE_PAR_CERTIFICATEUR", "false"),
52+
],
53+
},
54+
menuItem("Envoyées pour validation", "A_VALIDER_PAR_CERTIFICATEUR"),
55+
menuItem("Brouillons", "BROUILLON"),
56+
];
57+
3658
return (
3759
<div className="w-full flex flex-col md:flex-row gap-6">
3860
<SideMenu
@@ -42,9 +64,7 @@ const CertificationListLayout = ({ children }: { children: ReactNode }) => {
4264
sticky
4365
fullHeight
4466
items={[
45-
menuItem("Toutes les certifications", null),
46-
menuItem("Certifications disponibles", "AVAILABLE"),
47-
menuItem("Certifications inactives", "INACTIVE"),
67+
...items,
4868
{
4969
isActive: false,
5070
linkProps: {

packages/reva-admin-react/src/app/(admin)/certifications-v2/(list)/page.tsx

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,34 @@ import { WhiteCard } from "@/components/card/white-card/WhiteCard";
33
import { useGraphQlClient } from "@/components/graphql/graphql-client/GraphqlClient";
44
import { SearchList } from "@/components/search/search-list/SearchList";
55
import { graphql } from "@/graphql/generated";
6-
import { CertificationStatus } from "@/graphql/generated/graphql";
6+
import { CertificationStatusV2 } from "@/graphql/generated/graphql";
77
import { useQuery } from "@tanstack/react-query";
88
import { useSearchParams } from "next/navigation";
9-
import { Tag } from "@codegouvfr/react-dsfr/Tag";
109
import { Button } from "@codegouvfr/react-dsfr/Button";
1110
import { format } from "date-fns";
11+
import Badge from "@codegouvfr/react-dsfr/Badge";
1212

1313
const getCertificationsQuery = graphql(`
14-
query getCertificationsForListPage(
14+
query getCertificationsV2ForListPage(
1515
$offset: Int
1616
$searchFilter: String
17-
$status: CertificationStatus
17+
$status: CertificationStatusV2
18+
$visible: Boolean
1819
) {
19-
searchCertificationsForAdmin(
20+
searchCertificationsV2ForAdmin(
2021
limit: 10
2122
offset: $offset
2223
searchText: $searchFilter
2324
status: $status
25+
visible: $visible
2426
) {
2527
rows {
2628
id
2729
label
2830
codeRncp
2931
status
32+
statusV2
33+
visible
3034
certificationAuthorityStructure {
3135
label
3236
}
@@ -50,22 +54,27 @@ const CertificationListPage = () => {
5054
const currentPage = page ? Number.parseInt(page) : 1;
5155
const searchFilter = searchParams.get("search") || "";
5256
const status = searchParams.get("status");
57+
const stringParam = searchParams.get("visible");
58+
const visible =
59+
stringParam === "true" ? true : stringParam === "false" ? false : undefined;
5360

5461
const {
5562
data: getCertificationsResponse,
5663
status: getCertificationsQueryStatus,
5764
} = useQuery({
58-
queryKey: ["getCertifications", searchFilter, currentPage, status],
65+
queryKey: ["getCertifications", searchFilter, currentPage, status, visible],
5966
queryFn: () =>
6067
graphqlClient.request(getCertificationsQuery, {
6168
offset: (currentPage - 1) * RECORDS_PER_PAGE,
6269
searchFilter,
63-
status: status as CertificationStatus,
70+
status: status as CertificationStatusV2,
71+
visible,
6472
}),
6573
});
6674

6775
const certificationPage =
68-
getCertificationsResponse?.searchCertificationsForAdmin;
76+
getCertificationsResponse?.searchCertificationsV2ForAdmin;
77+
6978
return (
7079
certificationPage && (
7180
<div className="flex flex-col w-full">
@@ -84,18 +93,16 @@ const CertificationListPage = () => {
8493
>
8594
{(c) => (
8695
<WhiteCard key={c.id} className="gap-2">
87-
<span className="text-gray-500 text-sm">{c.codeRncp}</span>
96+
<div className="flex flex-row justify-between items-center">
97+
<span className="text-gray-500 text-sm">{c.codeRncp}</span>
98+
<BadgeCertificationStatus
99+
status={c.statusV2}
100+
visible={c.visible}
101+
/>
102+
</div>
88103
<span className="text-lg font-bold">{c.label}</span>
89104
<span>{c.certificationAuthorityStructure?.label}</span>
90105
<span>Expire le: {format(c.expiresAt, "dd/MM/yyyy")}</span>
91-
<Tag
92-
small
93-
className={`mt-2 text-black ${
94-
c.status === "AVAILABLE" ? "bg-green-300" : "bg-red-400"
95-
} `}
96-
>
97-
{c.status === "AVAILABLE" ? "Disponible" : "Inactive"}
98-
</Tag>
99106
<Button
100107
data-test="access-certification-button"
101108
className="mt-2 ml-auto"
@@ -114,4 +121,55 @@ const CertificationListPage = () => {
114121
);
115122
};
116123

124+
const BadgeCertificationStatus = ({
125+
status,
126+
visible,
127+
}: {
128+
status: CertificationStatusV2;
129+
visible: boolean;
130+
}) => (
131+
<>
132+
{status == "BROUILLON" && (
133+
<Badge
134+
noIcon
135+
severity="new"
136+
data-test="certification-timeline-element-brouillon-badge"
137+
>
138+
BROUILLON
139+
</Badge>
140+
)}
141+
142+
{status == "A_VALIDER_PAR_CERTIFICATEUR" && (
143+
<Badge
144+
noIcon
145+
severity="info"
146+
data-test="certification-timeline-element-envoye-pour-validation-badge"
147+
>
148+
ENVOYÉ POUR VALIDATION
149+
</Badge>
150+
)}
151+
152+
{((status == "VALIDE_PAR_CERTIFICATEUR" && !visible) ||
153+
status == "INACTIVE") && (
154+
<Badge
155+
noIcon
156+
severity="warning"
157+
data-test="certification-timeline-element-invisible-badge"
158+
>
159+
INVISIBLE
160+
</Badge>
161+
)}
162+
163+
{status == "VALIDE_PAR_CERTIFICATEUR" && visible && (
164+
<Badge
165+
noIcon
166+
severity="success"
167+
data-test="certification-timeline-element-visible-badge"
168+
>
169+
VISIBLE
170+
</Badge>
171+
)}
172+
</>
173+
);
174+
117175
export default CertificationListPage;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { $Enums, Prisma } from "@prisma/client";
2+
3+
import { prismaClient } from "../../../prisma/client";
4+
import { processPaginationInfo } from "../../shared/list/pagination";
5+
import { Certification, CertificationStatusV2 } from "../referential.types";
6+
7+
export const searchCertificationsV2ForAdmin = async ({
8+
offset,
9+
limit,
10+
searchText,
11+
status,
12+
visible,
13+
}: {
14+
offset?: number;
15+
limit?: number;
16+
searchText?: string;
17+
status?: CertificationStatusV2;
18+
visible?: boolean;
19+
}): Promise<PaginatedListResult<Certification>> => {
20+
const realLimit = limit || 10;
21+
const realOffset = offset || 0;
22+
23+
let whereClause: Prisma.CertificationWhereInput = {};
24+
25+
if (searchText) {
26+
whereClause = {
27+
...whereClause,
28+
OR: [
29+
{ label: { contains: searchText, mode: "insensitive" } },
30+
{ rncpId: { contains: searchText, mode: "insensitive" } },
31+
{
32+
certificationAuthorityStructure: {
33+
label: {
34+
contains: searchText,
35+
mode: "insensitive",
36+
},
37+
},
38+
},
39+
{
40+
rncpTypeDiplome: { contains: searchText, mode: "insensitive" },
41+
},
42+
],
43+
};
44+
}
45+
46+
if (status) {
47+
whereClause = {
48+
...whereClause,
49+
statusV2: status as $Enums.CertificationStatusV2,
50+
};
51+
}
52+
53+
if (visible) {
54+
whereClause = {
55+
...whereClause,
56+
visible,
57+
};
58+
}
59+
60+
const certifications = await prismaClient.certification.findMany({
61+
where: whereClause,
62+
orderBy: [{ label: "asc" }],
63+
take: limit,
64+
skip: offset,
65+
});
66+
67+
const count = await prismaClient.certification.count({
68+
where: whereClause,
69+
});
70+
71+
return {
72+
rows: certifications.map((c) => ({ ...c, codeRncp: c.rncpId })),
73+
info: processPaginationInfo({
74+
totalRows: count,
75+
limit: realLimit,
76+
offset: realOffset,
77+
}),
78+
};
79+
};

packages/reva-api/modules/referential/referential.graphql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Certification {
2323
codeRncp: String!
2424
status: CertificationStatus!
2525
statusV2: CertificationStatusV2!
26+
visible: Boolean!
2627
typeDiplome: String
2728
degree: Degree!
2829
conventionsCollectives: [ConventionCollective!]!
@@ -268,6 +269,13 @@ type Query {
268269
searchText: String
269270
status: CertificationStatus
270271
): CertificationPage!
272+
searchCertificationsV2ForAdmin(
273+
offset: Int
274+
limit: Int
275+
searchText: String
276+
status: CertificationStatusV2
277+
visible: Boolean
278+
): CertificationPage!
271279
getCertification(certificationId: ID!): Certification!
272280
getRegions: [Region!]!
273281
getDepartments: [Department!]!

packages/reva-api/modules/referential/referential.resolvers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { getReorientationReasons } from "./features/getReorientationReasons";
1616
import { getVulnerabilityIndicators } from "./features/getVulnerabilityIndicators";
1717
import { replaceCertification } from "./features/replaceCertification";
1818
import { searchCertificationsForAdmin } from "./features/searchCertificationsForAdmin";
19+
import { searchCertificationsV2ForAdmin } from "./features/searchCertificationsV2ForAdmin";
1920
import { updateCertification } from "./features/updateCertification";
2021
import { updateCompetenceBlocsByCertificationId } from "./features/updateCompetenceBlocsByCertificationId";
2122
import { referentialResolversSecurityMap } from "./referential.security";
@@ -133,6 +134,14 @@ const unsafeReferentialResolvers = {
133134
searchText: payload.searchText,
134135
status: payload.status,
135136
}),
137+
searchCertificationsV2ForAdmin: (_: any, payload: any) =>
138+
searchCertificationsV2ForAdmin({
139+
offset: payload.offset,
140+
limit: payload.limit,
141+
searchText: payload.searchText,
142+
status: payload.status,
143+
visible: payload.visible,
144+
}),
136145
getCertification: (
137146
_: unknown,
138147
{ certificationId }: { certificationId: string },

packages/reva-api/modules/referential/referential.types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ export const CertificationStatus = {
2222
export type CertificationStatus =
2323
(typeof CertificationStatus)[keyof typeof CertificationStatus];
2424

25+
export const CertificationStatusV2 = {
26+
BROUILLON: "BROUILLON",
27+
A_VALIDER_PAR_CERTIFICATEUR: "A_VALIDER_PAR_CERTIFICATEUR",
28+
VALIDE_PAR_CERTIFICATEUR: "VALIDE_PAR_CERTIFICATEUR",
29+
INACTIVE: "INACTIVE",
30+
};
31+
32+
export type CertificationStatusV2 =
33+
(typeof CertificationStatusV2)[keyof typeof CertificationStatusV2];
34+
2535
export interface Certification {
2636
id: string;
2737
label: string;
@@ -33,6 +43,8 @@ export interface Certification {
3343
abilities: string | null;
3444
codeRncp: string;
3545
status: CertificationStatus;
46+
statusV2: CertificationStatusV2;
47+
visible: boolean | null;
3648
}
3749

3850
export interface Region {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "certification" ADD COLUMN "visible" BOOLEAN NOT NULL DEFAULT false;

packages/reva-api/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ model Certification {
4646
summary String?
4747
status CertificationStatus @default(INACTIVE)
4848
statusV2 CertificationStatusV2 @default(BROUILLON) @map("status_v2")
49+
visible Boolean @default(false)
4950
rncpId String @map("rncp_id") @db.VarChar(255)
5051
organismsAndRegions OrganismsOnRegionsAndCertifications[]
5152
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)

0 commit comments

Comments
 (0)