Skip to content

Commit 77f3fba

Browse files
committed
feat(FON-241): session status
1 parent eeafe4e commit 77f3fba

14 files changed

Lines changed: 149 additions & 29 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- @param $1:sessionIds
2+
3+
SELECT s.id
4+
FROM nominations_context.session AS s
5+
WHERE
6+
s.id = ANY(/* sessionIds */$1::UUID[])
7+
AND s.deleted_at IS NULL
8+
AND s.archived_at IS NULL
9+
AND s.is_validated = TRUE
10+
11+
AND NOT EXISTS (
12+
SELECT 1
13+
FROM nominations_context.dossier_de_nomination
14+
WHERE session_id = s.id AND outcome != ALL('{VALIDATED,NON_VALIDATED,WITHDRAWN}'::nominations_context.nomination_file_outcome_enum[])
15+
)
16+
17+
AND NOT EXISTS (
18+
SELECT 1
19+
FROM nominations_context.dossier_de_nomination ddn
20+
LEFT JOIN docs.official_report_nomination_file ornf ON ornf.nomination_file_id = ddn.id
21+
WHERE ddn.session_id = s.id AND ornf.nomination_file_id IS NULL
22+
);

apps/api/src/modules/session/infrastructure/queries/detail-nomination-session.query.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import z from 'zod';
55
import { dateOnlyJsonSchema, Magistrat, TypeDeSaisine } from 'shared-models';
66

77
import { AffectationVersionFinder } from '../finders/affectation-version.finder';
8+
import { UnreportedSessionFilesCountFinder } from '../finders/count-unreported-files.finder';
89
import { Prisma } from 'src/generated/prisma/client';
910
import { PrismaService } from 'src/modules/framework/database';
1011
import { prismaFormationEnumToFormationEnum } from 'src/modules/shared/mappers/formation.mapper';
@@ -16,6 +17,7 @@ export class DetailNominationSessionQuery {
1617
constructor(
1718
private readonly prisma: PrismaService,
1819
private readonly affectationVersionFinder: AffectationVersionFinder,
20+
private readonly unreportedSessionFilesCountFinder: UnreportedSessionFilesCountFinder,
1921
) {}
2022

2123
async handle(query: {
@@ -59,6 +61,12 @@ export class DetailNominationSessionQuery {
5961
const affectationsCount = session.affectationVersions[0]?._count.affectations ?? 0;
6062
const isDeletable = session._count.attachments === 0 && affectationsCount === 0;
6163

64+
const unreportedCount = await this.unreportedSessionFilesCountFinder.find({
65+
sessionId: query.sessionId,
66+
tx: query.tx,
67+
});
68+
const isArchivable = session.isValidated && !session.archivedAt && unreportedCount === 0;
69+
6270
return {
6371
id: session.id,
6472
name: session.name,
@@ -73,6 +81,7 @@ export class DetailNominationSessionQuery {
7381
isValidated: session.isValidated,
7482
isDeletable,
7583
isArchived: !!session.archivedAt,
84+
isArchivable,
7685
};
7786
}
7887
}
@@ -90,5 +99,6 @@ export class DetailedNominationSessionDto extends createZodDto(
9099
isValidated: z.boolean(),
91100
isDeletable: z.boolean(),
92101
isArchived: z.boolean(),
102+
isArchivable: z.boolean(),
93103
}),
94104
) {}

apps/api/src/modules/session/infrastructure/queries/list-nomination-sessions.query.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import { dateOnlyJsonSchema, Magistrat, TypeDeSaisine } from 'shared-models';
55

66
import { ListGdsNominationSessionsQueryDto } from '../dtos/nomination-session.dto';
77
import { Prisma } from 'src/generated/prisma/client';
8+
import { findReportedSessionIds } from 'src/generated/prisma/sql';
89
import { PrismaService } from 'src/modules/framework/database';
910
import { createPaginatedZodDto, paginate, Pagination } from 'src/modules/framework/pagination';
1011
import { Sortable } from 'src/modules/framework/sorting';
1112
import { prismaFormationEnumToFormationEnum } from 'src/modules/shared/mappers/formation.mapper';
1213
import { prismaTypeDeSaisineEnumToTypeDeSaisine } from 'src/modules/shared/mappers/type-de-saisine-enum.mapper';
1314
import { DateOnly } from 'src/utils/date-only';
1415

15-
const SESSION_STATUSES = ['TO_VALIDATE', 'READY'] as const;
16+
const SESSION_STATUSES = ['TO_VALIDATE', 'READY', 'REPORTED'] as const;
1617
type SessionStatus = (typeof SESSION_STATUSES)[number];
1718

1819
@Injectable()
@@ -28,6 +29,7 @@ export class ListNominationSessionsQuery {
2829
}): Promise<ListedNominationSessionsDto> {
2930
const where: Prisma.SessionWhereInput = {
3031
deletedAt: null,
32+
archivedAt: null,
3133
typeDeSaisine: query.typeDeSaisine,
3234
...(query.formations?.length && {
3335
formation: { in: [...query.formations] },
@@ -38,16 +40,12 @@ export class ListNominationSessionsQuery {
3840
};
3941

4042
const orderBy: Prisma.SessionOrderByWithRelationInput[] = query.sorting.sortBy
41-
? [
42-
{
43-
[query.sorting.sortBy]: query.sorting.sortDesc ? ('desc' as const) : ('asc' as const),
44-
},
45-
]
43+
? [{ [query.sorting.sortBy]: query.sorting.sortDesc ? ('desc' as const) : ('asc' as const) }]
4644
: [{ date: 'desc' as const }, { createdAt: 'asc' as const }];
4745

48-
const [totalCount, sessions] = await this.prisma.$transaction([
49-
this.prisma.session.count({ where }),
50-
this.prisma.session.findMany({
46+
const [totalCount, sessions, reportedIds] = await this.prisma.$transaction(async (tx) => {
47+
const txCount = await tx.session.count({ where });
48+
const txSessions = await tx.session.findMany({
5149
where,
5250
orderBy,
5351
skip: (query.pagination.page - 1) * query.pagination.limit,
@@ -61,8 +59,13 @@ export class ListNominationSessionsQuery {
6159
typeDeSaisine: true,
6260
isValidated: true,
6361
},
64-
}),
65-
]);
62+
});
63+
64+
const reportedRows = await tx.$queryRawTyped(findReportedSessionIds(txSessions.map((s) => s.id)));
65+
const txReportedIds = new Set(reportedRows.map(({ id }) => id));
66+
67+
return [txCount, txSessions, txReportedIds];
68+
});
6669

6770
const items = sessions.map((s) => ({
6871
id: s.id,
@@ -71,14 +74,18 @@ export class ListNominationSessionsQuery {
7174
date: DateOnly.fromDate(s.date).toJson(),
7275
dueDate: s.dueDate ? DateOnly.fromDate(s.dueDate).toJson() : null,
7376
typeDeSaisine: prismaTypeDeSaisineEnumToTypeDeSaisine(s.typeDeSaisine),
74-
status: ListNominationSessionsQuery.computeStatus(s),
77+
status: ListNominationSessionsQuery.computeStatus(s, reportedIds),
7578
}));
7679

7780
return paginate({ items, totalCount, pagination: query.pagination });
7881
}
7982

80-
private static computeStatus(session: { isValidated: boolean }): SessionStatus {
83+
private static computeStatus(
84+
session: { id: string; isValidated: boolean },
85+
reportedIds: ReadonlySet<string>,
86+
): SessionStatus {
8187
if (!session.isValidated) return 'TO_VALIDATE';
88+
if (reportedIds.has(session.id)) return 'REPORTED';
8289
return 'READY';
8390
}
8491
}

apps/client/src/components/reports/components/ReportList/ReportListPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { generatePath, useParams } from 'react-router';
77

88
import { PageContentLayout } from '../../../shared/PageContentLayout';
99
import { useDataTable, useQueryDataTableState } from '@/components/shared/data-table';
10-
import { ArchiveBannerPortal } from '@/components/shared/layouts/archived-banner/ArchivedSessionUpdater';
10+
import { ArchiveBannerPortal } from '@/components/shared/layouts/archived-banner/ArchiveBannerPortal';
1111
import { ObservationLinks } from '@/components/shared/ObservationLinks';
1212
import { PriorityBadgeList } from '@/components/shared/priorities/PriorityBadge';
1313
import {

apps/client/src/components/reports/components/ReportOverview/ReportOverview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from '../../../../utils/transparences-breadcrumb.utils';
99
import { Breadcrumb } from '../../../shared/Breadcrumb';
1010
import { ScrollToTop } from '../../../shared/ScrollToTop';
11-
import { ArchiveBannerPortal } from '@/components/shared/layouts/archived-banner/ArchivedSessionUpdater';
11+
import { ArchiveBannerPortal } from '@/components/shared/layouts/archived-banner/ArchiveBannerPortal';
1212
import type { ReportStatusEnum } from '@/types/enums.types';
1313
import {
1414
useAttachReportFilesMutation,

apps/client/src/components/secretariat-general/session/SessionStatusBadge.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1+
import { colors } from '@codegouvfr/react-dsfr';
12
import Badge from '@codegouvfr/react-dsfr/Badge';
23

3-
export function SessionStatusBadge(props: { status: 'READY' | 'TO_VALIDATE' }) {
4+
const reportedBgColor = colors.decisions.artwork.background.purpleGlycine.default;
5+
const reportedTextColor = colors.decisions.text.actionHigh.purpleGlycine.default;
6+
7+
export function SessionStatusBadge(props: { status: 'READY' | 'TO_VALIDATE' | 'REPORTED' }) {
48
switch (props.status) {
59
case 'TO_VALIDATE':
610
return (
711
<Badge small severity="new">
812
NOUVEAU
913
</Badge>
1014
);
15+
case 'REPORTED':
16+
return (
17+
<Badge noIcon as="span" small style={{ color: reportedTextColor, background: reportedBgColor }}>
18+
RESTITUÉE
19+
</Badge>
20+
);
1121
default:
1222
return (
1323
<Badge small severity="info" noIcon>

apps/client/src/components/secretariat-general/transparence/content/Transparence.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useParams } from 'react-router';
44

55
import { AlertsProvider } from '@/components/shared/alerts/AlertsProvider';
66
import { Breadcrumb } from '@/components/shared/Breadcrumb';
7-
import { ArchiveBannerPortal } from '@/components/shared/layouts/archived-banner/ArchivedSessionUpdater';
7+
import { ArchiveBannerPortal } from '@/components/shared/layouts/archived-banner/ArchiveBannerPortal';
88
import { NominationFilesTable } from '@/components/shared/nomination-files-table/NominationFilesTable';
99
import type { BreadcrumbVM } from '@/models/breadcrumb-vm.model';
1010
import { ROUTE_PATHS } from '@/utils/route-path.utils';

apps/client/src/components/secretariat-general/transparence/content/tableau-de-bord/resume/TableauDeBordResume.tsx

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { dateOnlyToDate } from '@/utils/date-only.util';
1212
import { ROUTE_PATHS } from '@/utils/route-path.utils';
1313
import type { DetailedNominationSessionDto } from '@api/types';
1414
import {
15+
useArchiveNominationSessionMutation,
1516
useDeleteNominationSessionMutation,
1617
useListNominationFilesAsExcelMutation,
1718
} from '@queries/nomination-sessions.queries';
@@ -23,8 +24,34 @@ export const TableauDeBordResume = (transparence: DetailedNominationSessionDto)
2324
const navigate = useNavigate();
2425
const confirmation = useConfirmation();
2526

26-
const { mutate: exportAsExcel } = useListNominationFilesAsExcelMutation();
27+
const exportAsExcelMutation = useListNominationFilesAsExcelMutation();
2728
const deleteSessionMutation = useDeleteNominationSessionMutation({ sessionId: transparence.id });
29+
const archiveSessionMutation = useArchiveNominationSessionMutation({ sessionId: transparence.id });
30+
31+
const onArchive = React.useCallback(async () => {
32+
const { isConfirmed } = await confirmation.waitForConfirmation({
33+
title: `Confirmer l'archivage`,
34+
content: (
35+
<>
36+
<p>
37+
<FormattedMessage
38+
defaultMessage={'Vous allez archiver la transparence «\u00A0{name}\u00A0».'}
39+
values={{ name: transparence.name }}
40+
/>
41+
</p>
42+
<p>
43+
<FormattedMessage defaultMessage={'Souhaitez-vous continuer\u00A0?'} />
44+
</p>
45+
</>
46+
),
47+
});
48+
49+
if (!isConfirmed) return;
50+
51+
archiveSessionMutation.mutate(undefined, {
52+
onSuccess: () => navigate(ROUTE_PATHS.SG.MANAGE_SESSION),
53+
});
54+
}, [confirmation, transparence.name, archiveSessionMutation, navigate]);
2855

2956
const onDelete = React.useCallback(async () => {
3057
const { isConfirmed } = await confirmation.waitForConfirmation({
@@ -55,18 +82,21 @@ export const TableauDeBordResume = (transparence: DetailedNominationSessionDto)
5582
deleteSessionMutation.mutate(undefined, {
5683
onSuccess: () => navigate(ROUTE_PATHS.SG.MANAGE_SESSION),
5784
});
58-
}, [confirmation, transparence, deleteSessionMutation, navigate]);
85+
}, [confirmation, transparence.name, deleteSessionMutation, navigate]);
86+
87+
const isMutationPending =
88+
deleteSessionMutation.isPending || archiveSessionMutation.isPending || exportAsExcelMutation.isPending;
5989

6090
return (
6191
<div className="flex max-w-[63%] flex-col gap-y-2 px-2">
6292
<h1 className="mb-0 flex items-center justify-between gap-2">
6393
<span className="hyphens-auto">{transparence.name}</span>
64-
<MenuRoot>
94+
<MenuRoot disabled={isMutationPending}>
6595
<MenuTrigger
66-
disabled={deleteSessionMutation.isPending}
67-
iconId={deleteSessionMutation.isPending ? 'ri-loader-4-line' : 'ri-menu-fill'}
96+
disabled={isMutationPending}
97+
iconId={isMutationPending ? 'ri-loader-4-line' : 'ri-menu-fill'}
6898
className={clsx('shrink-0 grow-0 rounded-full', {
69-
"before:animate-spin before:content-['']": deleteSessionMutation.isPending,
99+
"before:animate-spin before:content-['']": isMutationPending,
70100
})}
71101
priority="tertiary no outline"
72102
title={`Actions sur la transparence "${transparence.name}"`}
@@ -84,6 +114,7 @@ export const TableauDeBordResume = (transparence: DetailedNominationSessionDto)
84114
Éditer
85115
</MenuItem>
86116
<MenuItem
117+
disabled={isMutationPending}
87118
iconId="fr-icon-file-add-line"
88119
nativeButtonProps={importAttachments.modal.buttonProps}
89120
>
@@ -92,17 +123,32 @@ export const TableauDeBordResume = (transparence: DetailedNominationSessionDto)
92123
</>
93124
)}
94125
<MenuItem
126+
disabled={isMutationPending}
95127
iconId="ri-file-download-line"
96128
onClick={() => {
97-
exportAsExcel({ sessionId: transparence.id });
129+
exportAsExcelMutation.mutate({ sessionId: transparence.id });
98130
}}
99131
>
100132
Export .xlsx
101133
</MenuItem>
102134

103-
{transparence.isDeletable && (
135+
{transparence.isArchivable && (
136+
<MenuItem
137+
disabled={isMutationPending}
138+
nativeButtonProps={confirmation.buttonProps}
139+
iconId={archiveSessionMutation.isPending ? 'ri-loader-4-fill' : 'fr-icon-archive-fill'}
140+
onClick={onArchive}
141+
className={clsx({
142+
"before:animate-spin before:content-['']": archiveSessionMutation.isPending,
143+
})}
144+
>
145+
Archiver
146+
</MenuItem>
147+
)}
148+
149+
{!isArchived && transparence.isDeletable && (
104150
<MenuItem
105-
disabled={deleteSessionMutation.isPending}
151+
disabled={isMutationPending}
106152
nativeButtonProps={confirmation.buttonProps}
107153
iconId={deleteSessionMutation.isPending ? `ri-loader-4-fill` : 'ri-delete-bin-fill'}
108154
onClick={onDelete}

apps/client/src/components/shared/layouts/archived-banner/ArchivedSessionUpdater.tsx renamed to apps/client/src/components/shared/layouts/archived-banner/ArchiveBannerPortal.tsx

File renamed without changes.

apps/client/src/generated/api/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export type ListedNominationSessionsDto = {
260260
day: number;
261261
} | null;
262262
typeDeSaisine: 'TRANSPARENCE_GDS';
263-
status: 'TO_VALIDATE' | 'READY';
263+
status: 'TO_VALIDATE' | 'READY' | 'REPORTED';
264264
}>;
265265
totalCount: number;
266266
currentPageIndex: number;
@@ -500,6 +500,7 @@ export type DetailedNominationSessionDto = {
500500
isValidated: boolean;
501501
isDeletable: boolean;
502502
isArchived: boolean;
503+
isArchivable: boolean;
503504
};
504505

505506
export type UpdateNominationSessionDto = {

0 commit comments

Comments
 (0)