From ef9e870ecae2922e3413ba6a476f3fa88178b5e3 Mon Sep 17 00:00:00 2001 From: Maxime Naulleau Date: Thu, 7 Nov 2024 18:22:14 +0100 Subject: [PATCH] Add observers in reports list page and refactor the frontend builders --- .../nestjs/reporter.controller.e2e-spec.ts | 1 + .../sql-report-listing-vm.query.it-spec.ts | 1 + .../drizzle/sql-report-listing-vm.query.ts | 3 ++ .../sql-report-retrieval-vm.query.it-spec.ts | 2 +- .../drizzle/sql-report-retrieval-vm.query.ts | 5 +- .../models/report-retrieval-vm.builder.ts | 18 +++++-- .../business-logic/models/report.builder.ts | 9 +++- .../list-reports.use-case.spec.ts | 1 + apps/webapp/package.json | 2 +- .../NominationFileList.spec.tsx | 4 +- .../NominationFilesTable.tsx | 4 +- .../NominationFileOverview.spec.tsx | 35 ++++++++++--- .../NominationFileOverview.tsx | 2 + .../NominationFileOverview/Observers.tsx | 47 +++++++++++++++++ .../selectors/selectNominationFile.spec.ts | 17 +++++-- .../primary/selectors/selectNominationFile.ts | 10 +++- .../selectNominationFileList.spec.ts | 8 +-- .../selectors/selectNominationFileList.ts | 4 +- .../ApiNominationFile.gateway.spec.ts | 32 +++--------- .../gateways/ApiNominationFile.gateway.ts | 2 + .../gateways/FakeNominationFile.client.ts | 6 ++- .../gateways/FakeNominationFile.gateway.ts | 50 ++++++++++++++----- .../builders/NominationFile.builder.ts | 26 ++++++++-- .../builders/NominationFileVM.builder.ts | 17 ++++++- .../listNominationFile.use-case.spec.ts | 3 +- .../retrieveNominationFile.use-case.spec.ts | 4 +- .../updateNominationFile.use-case.spec.ts | 2 +- .../updateNominationRule.use-case.spec.ts | 10 ++-- .../view-models/NominationFileVM.ts | 1 + .../src/nomination-file/store/appState.ts | 5 +- apps/webapp/src/router/AppRouter.spec.tsx | 10 ++-- apps/webapp/tsconfig.app.json | 2 +- apps/webapp/vitest.config.ts | 2 +- apps/webapp/vitest/vitest.setup.ts | 1 + .../models/report-retrieval-vm.ts | 1 + .../models/reports-listing-vm.ts | 1 + 36 files changed, 260 insertions(+), 88 deletions(-) create mode 100644 apps/webapp/src/nomination-file/adapters/primary/components/NominationFileOverview/Observers.tsx create mode 100644 apps/webapp/vitest/vitest.setup.ts diff --git a/apps/api/src/reporter-context/adapters/primary/nestjs/reporter.controller.e2e-spec.ts b/apps/api/src/reporter-context/adapters/primary/nestjs/reporter.controller.e2e-spec.ts index 0862e4f..5341428 100644 --- a/apps/api/src/reporter-context/adapters/primary/nestjs/reporter.controller.e2e-spec.ts +++ b/apps/api/src/reporter-context/adapters/primary/nestjs/reporter.controller.e2e-spec.ts @@ -88,6 +88,7 @@ describe('Reporter Controller', () => { transparency: Transparency.MARCH_2025, grade: Magistrat.Grade.HH, targettedPosition: 'a position', + observersCount: 1, }; const aReport = ReportBuilder.fromListingVM(aReportListingVM) .withNominationFileId('ca1619e2-263d-49b6-b928-6a04ee681138') diff --git a/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-listing-vm.query.it-spec.ts b/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-listing-vm.query.it-spec.ts index 7b3a59a..84ae649 100644 --- a/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-listing-vm.query.it-spec.ts +++ b/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-listing-vm.query.it-spec.ts @@ -64,6 +64,7 @@ describe('SQL Report Listing VM Query', () => { transparency: aReport.transparency, grade: aReport.grade, targettedPosition: aReport.targettedPosition, + observersCount: aReport.observers?.length || 0, }, ], }); diff --git a/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-listing-vm.query.ts b/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-listing-vm.query.ts index fab428f..891b70d 100644 --- a/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-listing-vm.query.ts +++ b/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-listing-vm.query.ts @@ -3,6 +3,7 @@ import { ReportListingVMQuery } from 'src/reporter-context/business-logic/gatewa import { DrizzleDb } from 'src/shared-kernel/adapters/secondary/gateways/repositories/drizzle/config/drizzle-instance'; import { DateOnly } from 'src/shared-kernel/business-logic/models/date-only'; import { reports } from './schema/report-pm'; +import { sql } from 'drizzle-orm'; export class SqlReportListingVMQuery implements ReportListingVMQuery { constructor(private readonly db: DrizzleDb) {} @@ -19,6 +20,7 @@ export class SqlReportListingVMQuery implements ReportListingVMQuery { transparency: reports.transparency, grade: reports.grade, targettedPosition: reports.targettedPosition, + observersCount: sql`COALESCE(array_length(${reports.observers}, 1), 0)`, }) .from(reports) .execute(); @@ -36,6 +38,7 @@ export class SqlReportListingVMQuery implements ReportListingVMQuery { transparency: report.transparency, grade: report.grade, targettedPosition: report.targettedPosition, + observersCount: report.observersCount, })), }; } diff --git a/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-retrieval-vm.query.it-spec.ts b/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-retrieval-vm.query.it-spec.ts index 18c9f89..8ef0a15 100644 --- a/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-retrieval-vm.query.it-spec.ts +++ b/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-retrieval-vm.query.it-spec.ts @@ -90,7 +90,7 @@ describe('SQL Report Retrieval VM Query', () => { it('retrieves with empty values', async () => { const expectedRules = prepareExpectedRules(aReportRule); const result = await sqlReportRetrievalVMQuery.retrieveReport(aReport.id); - expect(result).toEqual( + expect(result).toEqual( ReportRetrievalVMBuilder.fromWriteModel(aReport) .withDueDate(null) .withComment(null) diff --git a/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-retrieval-vm.query.ts b/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-retrieval-vm.query.ts index 0c1c359..16349ff 100644 --- a/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-retrieval-vm.query.ts +++ b/apps/api/src/reporter-context/adapters/secondary/gateways/repositories/drizzle/sql-report-retrieval-vm.query.ts @@ -1,5 +1,5 @@ import { NominationFile, ReportRetrievalVM } from 'shared-models'; -import { eq } from 'drizzle-orm'; +import { eq, sql } from 'drizzle-orm'; import { ReportRetrievalVMQuery } from 'src/reporter-context/business-logic/gateways/queries/report-retrieval-vm.query'; import { DrizzleDb } from 'src/shared-kernel/adapters/secondary/gateways/repositories/drizzle/config/drizzle-instance'; import { DateOnly } from 'src/shared-kernel/business-logic/models/date-only'; @@ -25,6 +25,8 @@ export class SqlReportRetrievalVMQuery implements ReportRetrievalVMQuery { targettedPosition: reports.targettedPosition, comment: reports.comment, rank: reports.rank, + observers: reports.observers, + observersCount: sql`COALESCE(array_length(${reports.observers}, 1), 0)`, // Rule fields ruleId: reportRules.id, ruleGroup: reportRules.ruleGroup, @@ -93,6 +95,7 @@ export class SqlReportRetrievalVMQuery implements ReportRetrievalVMQuery { targettedPosition: reportData.targettedPosition, comment: reportData.comment ? reportData.comment : null, rank: reportData.rank, + observers: reportData.observers, rules, }; diff --git a/apps/api/src/reporter-context/business-logic/models/report-retrieval-vm.builder.ts b/apps/api/src/reporter-context/business-logic/models/report-retrieval-vm.builder.ts index 63498ca..e2bcd19 100644 --- a/apps/api/src/reporter-context/business-logic/models/report-retrieval-vm.builder.ts +++ b/apps/api/src/reporter-context/business-logic/models/report-retrieval-vm.builder.ts @@ -22,6 +22,8 @@ export class ReportRetrievalVMBuilder { private comment: string | null; private rank: string; private rules: NominationFile.Rules; + private observers: string[] | null; + private observersCount: number; constructor() { this.id = 'report-id'; @@ -45,6 +47,7 @@ export class ReportRetrievalVMBuilder { this.targettedPosition = 'targetted position'; this.comment = 'comments'; this.rank = '(2 sur une liste de 100)'; + this.observers = ['observer 1', 'observer 2']; const defaultValue: NominationFile.RuleValue = { id: 'rule-id', @@ -94,11 +97,11 @@ export class ReportRetrievalVMBuilder { }; } - withId(id: string): this { + withId(id: string) { this.id = id; return this; } - withName(name: string): this { + withName(name: string) { this.name = name; return this; } @@ -146,6 +149,11 @@ export class ReportRetrievalVMBuilder { this.rank = rank; return this; } + withObservers(observers: string[] | null) { + this.observers = observers; + this.observersCount = observers?.length || 0; + return this; + } withOverseasToOverseasRule(options: Partial): this { const rule = this.rules.management[NominationFile.ManagementRule.OVERSEAS_TO_OVERSEAS]; @@ -156,7 +164,7 @@ export class ReportRetrievalVMBuilder { }; return this; } - withRules(rules: NominationFile.Rules): this { + withRules(rules: NominationFile.Rules) { this.rules = rules; return this; } @@ -176,6 +184,7 @@ export class ReportRetrievalVMBuilder { targettedPosition: this.targettedPosition, comment: this.comment, rank: this.rank, + observers: this.observers, rules: this.rules, }; } @@ -196,6 +205,7 @@ export class ReportRetrievalVMBuilder { .withCurrentPosition(report.currentPosition) .withTargettedPosition(report.targettedPosition) .withComment(report.comment) - .withRank(report.rank); + .withRank(report.rank) + .withObservers(report.observers); } } diff --git a/apps/api/src/reporter-context/business-logic/models/report.builder.ts b/apps/api/src/reporter-context/business-logic/models/report.builder.ts index 05edfcd..06086b7 100644 --- a/apps/api/src/reporter-context/business-logic/models/report.builder.ts +++ b/apps/api/src/reporter-context/business-logic/models/report.builder.ts @@ -25,7 +25,7 @@ export class ReportBuilder { private comment: string | null; private rank: string; private reporterName: string | null; - private observers: string[]; + private observers: string[] | null; constructor() { this.id = 'report-id'; @@ -112,6 +112,10 @@ export class ReportBuilder { this.rank = rank; return this; } + withObservers(observers: string[] | null): ReportBuilder { + this.observers = observers; + return this; + } build(): NominationFileReport { return new NominationFileReport( @@ -183,6 +187,7 @@ export class ReportBuilder { .withCurrentPosition(reportRetrievalVM.currentPosition) .withTargettedPosition(reportRetrievalVM.targettedPosition) .withComment(reportRetrievalVM.comment) - .withRank(reportRetrievalVM.rank); + .withRank(reportRetrievalVM.rank) + .withObservers(reportRetrievalVM.observers); } } diff --git a/apps/api/src/reporter-context/business-logic/use-cases/report-listing/list-reports.use-case.spec.ts b/apps/api/src/reporter-context/business-logic/use-cases/report-listing/list-reports.use-case.spec.ts index 7203cd0..a2bc145 100644 --- a/apps/api/src/reporter-context/business-logic/use-cases/report-listing/list-reports.use-case.spec.ts +++ b/apps/api/src/reporter-context/business-logic/use-cases/report-listing/list-reports.use-case.spec.ts @@ -41,5 +41,6 @@ describe('List reports', () => { transparency: Transparency.MARCH_2025, grade: Magistrat.Grade.HH, targettedPosition: 'a position', + observersCount: 1, }; }); diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 52a9855..de5d979 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -11,7 +11,7 @@ "types:check": "tsc --noEmit -p ./tsconfig.app.json", "types:check:watch": "tsc --noEmit -p ./tsconfig.app.json --watch", "preview": "vite preview", - "test": "DEBUG_PRINT_LIMIT=10 vitest", + "test": "DEBUG_PRINT_LIMIT=1000 vitest", "test:all": "vitest run", "postinstall": "pnpm dsfr:build", "dsfr:build": "react-dsfr update-icons" diff --git a/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileList/NominationFileList.spec.tsx b/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileList/NominationFileList.spec.tsx index 87d0e88..f3ebaab 100644 --- a/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileList/NominationFileList.spec.tsx +++ b/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileList/NominationFileList.spec.tsx @@ -54,6 +54,7 @@ describe("Nomination Case List Component", () => { await screen.findByText("Transparence"); await screen.findByText("Grade actuel"); await screen.findByText("Poste ciblé"); + await screen.findByText("Observants"); }); it("shows it in the table", async () => { @@ -65,6 +66,7 @@ describe("Nomination Case List Component", () => { await screen.findByText("Mars 2025"); await screen.findByText("I"); await screen.findByText("PG TJ Marseille"); + await screen.findByText("2"); }); }); @@ -92,4 +94,4 @@ const user = { const aNominationFile = new NominationFileBuilder() .withReporterName(user.reporterName) - .build(); + .buildListVM(); diff --git a/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileList/NominationFilesTable.tsx b/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileList/NominationFilesTable.tsx index c886524..c63deda 100644 --- a/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileList/NominationFilesTable.tsx +++ b/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileList/NominationFilesTable.tsx @@ -19,6 +19,7 @@ export const NominationFilesTable: React.FC = ({ "Transparence", "Grade actuel", "Poste ciblé", + "Observants", ]} bordered data={nominationFiles.map((nominationFile) => [ @@ -29,8 +30,9 @@ export const NominationFilesTable: React.FC = ({ {nominationFile.name} ,
{nominationFile.transparency}
, -
{nominationFile.grade}
, +
{nominationFile.grade}
,
{nominationFile.targettedPosition}
, +
{nominationFile.observersCount}
, ])} /> ); diff --git a/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileOverview/NominationFileOverview.spec.tsx b/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileOverview/NominationFileOverview.spec.tsx index 3eff023..688d3f0 100644 --- a/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileOverview/NominationFileOverview.spec.tsx +++ b/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileOverview/NominationFileOverview.spec.tsx @@ -1,14 +1,13 @@ -import "@testing-library/jest-dom"; import { act, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Provider } from "react-redux"; import { NominationFile } from "shared-models"; import { NominationFileBuilder } from "../../../../core-logic/builders/NominationFile.builder"; +import { NominationFileVM } from "../../../../core-logic/view-models/NominationFileVM"; import { AppState } from "../../../../store/appState"; import { ReduxStore, initReduxStore } from "../../../../store/reduxStore"; import { FakeNominationFileGateway } from "../../../secondary/gateways/FakeNominationFile.gateway"; import { NominationFileOverview } from "./NominationFileOverview"; -import { NominationFileVM } from "../../../../core-logic/view-models/NominationFileVM"; describe("Nomination Case Overview Component", () => { let store: ReduxStore; @@ -57,6 +56,24 @@ describe("Nomination Case Overview Component", () => { await expectMagistratIdentity(); }); + it("shows the observers", async () => { + renderNominationFile(aValidatedNomination.id); + await screen.findByText("Observants"); + for (const [ + index, + observer, + ] of aValidatedNomination.observers!.entries()) { + if (index === 0) { + await screen.findByText(observer); + } else { + const observer2Name = await screen.findByText("observer 2"); + expect(observer2Name).toHaveClass("fr-text--bold"); + await screen.findByText("VPI TJ Rennes"); + await screen.findByText("(1 sur une liste de 2)"); + } + } + }); + it("shows the rules", async () => { act(() => { renderNominationFile(aValidatedNomination.id); @@ -101,7 +118,7 @@ describe("Nomination Case Overview Component", () => { const aNomination = new NominationFileBuilder() .withId("without-comment") .withComment(null) - .build(); + .buildRetrieveVM(); nominationFileGateway.addNominationFile(aNomination); renderNominationFile(aNomination.id); @@ -217,7 +234,9 @@ describe("Nomination Case Overview Component", () => { ]; it(`when checked, '${anotherRuleLabel}' can also be checked`, async () => { nominationFileGateway.addNominationFile( - new NominationFileBuilder().withTransferTimeValidated(false).build(), + new NominationFileBuilder() + .withTransferTimeValidated(false) + .buildRetrieveVM(), ); renderNominationFile("nomination-file-id"); @@ -305,8 +324,12 @@ describe("Nomination Case Overview Component", () => { const aValidatedNomination = new NominationFileBuilder() .withId("nomination-file-id") .withBiography(" - John Doe's biography - second line - third line ") - .build(); + .withObservers([ + "observer 1", + "observer 2\nVPI TJ Rennes\n(1 sur une liste de 2)", + ]) + .buildRetrieveVM(); const anUnvalidatedNomination = new NominationFileBuilder() .withAllRulesUnvalidated() - .build(); + .buildRetrieveVM(); diff --git a/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileOverview/NominationFileOverview.tsx b/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileOverview/NominationFileOverview.tsx index 9a0ee62..09c3419 100644 --- a/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileOverview/NominationFileOverview.tsx +++ b/apps/webapp/src/nomination-file/adapters/primary/components/NominationFileOverview/NominationFileOverview.tsx @@ -16,6 +16,7 @@ import { Comment } from "./Comment"; import { MagistratIdentity } from "./MagistratIdentity"; import { NominationRules } from "./NominationRules"; import { VMNominationFileRuleValue } from "../../../../core-logic/view-models/NominationFileVM"; +import { Observers } from "./Observers"; export type NominationFileOverviewProps = { id: string; @@ -89,6 +90,7 @@ export const NominationFileOverview: React.FC = ({ rank={nominationFile.rank} /> + ) => { + if (!observers) return null; + + return ( + + +
+ {observers.map(([observerName, ...observerInformation]) => ( +
+
+ {observerName} +
+ +
+ ))} +
+
+ ); +}; + +const ObserverInformation = ({ + observerInformation, +}: { + observerInformation: string[]; +}) => { + return ( +
+ {observerInformation.map((info) => ( +
{info}
+ ))} +
+ ); +}; diff --git a/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFile.spec.ts b/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFile.spec.ts index a545b13..e433c97 100644 --- a/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFile.spec.ts +++ b/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFile.spec.ts @@ -60,9 +60,18 @@ describe("Select Nomination Case", () => { .withName("John Doe") .withDueDate(new DateOnly(2030, 10, 30)) .withBiography("The biography.") + .withObservers([ + "observer 1", + "observer 2\nVPI TJ Rennes\n(1 sur une liste de 2)", + ]) + .buildRetrieveVM(); + const aNominationFileVM = NominationFileBuilderVM.fromStoreModel( + aNominationFile, + ) + .withAllRulesChecked(false) + .withObservers([ + ["observer 1"], + ["observer 2", "VPI TJ Rennes", "(1 sur une liste de 2)"], + ]) .build(); - const aNominationFileVM: NominationFileVM = - NominationFileBuilderVM.fromStoreModel(aNominationFile) - .withAllRulesChecked(false) - .build(); }); diff --git a/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFile.ts b/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFile.ts index 826080e..e3c101b 100644 --- a/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFile.ts +++ b/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFile.ts @@ -5,7 +5,7 @@ import { NominationFileVM, VMNominationFileRuleValue, } from "../../../core-logic/view-models/NominationFileVM"; -import { AppState } from "../../../store/appState"; +import { AppState, NominationFileSM } from "../../../store/appState"; type RuleCheckedEntry = NominationFileVM["rulesChecked"][NominationFile.RuleGroup]; @@ -65,7 +65,7 @@ export const selectNominationFile = createSelector( targettedPosition: nominationFile.targettedPosition, comment: nominationFile.comment, rank: nominationFile.rank, - + observers: formatObservers(nominationFile.observers), rulesChecked: { ...createRulesCheckedEntryFor(NominationFile.RuleGroup.MANAGEMENT), ...createRulesCheckedEntryFor(NominationFile.RuleGroup.STATUTORY), @@ -87,6 +87,12 @@ const formatBiography = (biography: string) => { return `- ${firstElement}\n- ${otherElements.join("\n- ")}`; }; +const formatObservers = ( + observers: NominationFileSM["observers"], +): NominationFileVM["observers"] => + observers?.map((observer) => observer.split("\n") as [string, ...string[]]) || + null; + const createRuleCheckedEntryFromValidatedRules = < G extends NominationFile.RuleGroup, >( diff --git a/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFileList.spec.ts b/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFileList.spec.ts index 8c7f53f..6b30d73 100644 --- a/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFileList.spec.ts +++ b/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFileList.spec.ts @@ -73,7 +73,7 @@ describe("Select Nomination Case List", () => { .withName("Banneau Louise") .withReporterName(user.reporterName) .withDueDate(new DateOnly(2030, 10, 30)) - .build(); + .buildListVM(); const aNominationFileVM: NominationFileListItemVM = { id: aNominationFile.id, name: aNominationFile.name, @@ -84,6 +84,7 @@ describe("Select Nomination Case List", () => { transparency: "Mars 2025", grade: "I", targettedPosition: "PG TJ Marseille", + observersCount: aNominationFile.observersCount, href: `/dossier-de-nomination/${aNominationFile.id}`, onClick, }; @@ -93,7 +94,7 @@ describe("Select Nomination Case List", () => { .withName("Denan Lucien") .withReporterName(user.reporterName) .withDueDate(new DateOnly(2030, 10, 30)) - .build(); + .buildListVM(); const anotherNominationFileVM: NominationFileListItemVM = { id: anotherNominationFile.id, name: anotherNominationFile.name, @@ -104,6 +105,7 @@ describe("Select Nomination Case List", () => { transparency: "Mars 2025", grade: "I", targettedPosition: "PG TJ Marseille", + observersCount: aNominationFile.observersCount, href: `/dossier-de-nomination/${anotherNominationFile.id}`, onClick, }; @@ -113,7 +115,7 @@ describe("Select Nomination Case List", () => { .withName("Another name") .withReporterName("ANOTHER REPORTER Name") .withDueDate(new DateOnly(2030, 10, 10)) - .build(); + .buildListVM(); }); const user = { diff --git a/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFileList.ts b/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFileList.ts index 7173279..aa9a2ff 100644 --- a/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFileList.ts +++ b/apps/webapp/src/nomination-file/adapters/primary/selectors/selectNominationFileList.ts @@ -12,7 +12,7 @@ export type NominationFileListItemVM = { transparency: ReturnType; grade: ReturnType; targettedPosition: string; - + observersCount: number; href: string; onClick: () => void; }; @@ -43,6 +43,7 @@ export const selectNominationFileList = createAppSelector( transparency, grade, targettedPosition, + observersCount, }) => { const { href, onClick } = getAnchorAttributes(id); @@ -64,6 +65,7 @@ export const selectNominationFileList = createAppSelector( transparency: transparencyToLabel(transparency), grade: gradeToLabel(grade), targettedPosition, + observersCount, href, onClick, } as const; diff --git a/apps/webapp/src/nomination-file/adapters/secondary/gateways/ApiNominationFile.gateway.spec.ts b/apps/webapp/src/nomination-file/adapters/secondary/gateways/ApiNominationFile.gateway.spec.ts index 1cc1c32..ba1f5fb 100644 --- a/apps/webapp/src/nomination-file/adapters/secondary/gateways/ApiNominationFile.gateway.spec.ts +++ b/apps/webapp/src/nomination-file/adapters/secondary/gateways/ApiNominationFile.gateway.spec.ts @@ -1,8 +1,8 @@ -import { Magistrat, NominationFile, Transparency } from "shared-models"; +import { NominationFile } from "shared-models"; +import { NominationFileBuilder } from "../../../core-logic/builders/NominationFile.builder"; import { NominationFileListItem } from "../../../store/appState"; import { ApiNominationFileGateway } from "./ApiNominationFile.gateway"; import { FakeNominationFileApiClient } from "./FakeNominationFile.client"; -import { FakeNominationFileFromApi } from "./FakeNominationFile.gateway"; describe("Api Nomination File Gateway", () => { let nominationFileApiClient: FakeNominationFileApiClient; @@ -26,6 +26,7 @@ describe("Api Nomination File Gateway", () => { transparency: aReport.transparency, grade: aReport.grade, targettedPosition: aReport.targettedPosition, + observersCount: aReport.observersCount, }, ]); }); @@ -51,6 +52,7 @@ describe("Api Nomination File Gateway", () => { targettedPosition: aReport.targettedPosition, comment: aReport.comment, rank: aReport.rank, + observers: aReport.observers, rules: { ...rules, [aRule.group]: { @@ -87,29 +89,9 @@ describe("Api Nomination File Gateway", () => { }); }); -const aReport: Omit = { - id: "report-id", - name: "name", - reporterName: "REPORTER Name", - biography: "biography", - dueDate: { - year: 2030, - month: 10, - day: 5, - }, - birthDate: { - year: 2030, - month: 10, - day: 5, - }, - state: NominationFile.ReportState.NEW, - formation: Magistrat.Formation.PARQUET, - transparency: Transparency.MARCH_2025, - grade: Magistrat.Grade.I, - currentPosition: "current position", - targettedPosition: "targetted position", - comment: "some comment", - rank: "some rank", +const aReport = { + ...new NominationFileBuilder().buildRetrieveVM(), + ...new NominationFileBuilder().buildListVM(), }; const aRule = { id: "1", diff --git a/apps/webapp/src/nomination-file/adapters/secondary/gateways/ApiNominationFile.gateway.ts b/apps/webapp/src/nomination-file/adapters/secondary/gateways/ApiNominationFile.gateway.ts index c5dc5de..383210c 100644 --- a/apps/webapp/src/nomination-file/adapters/secondary/gateways/ApiNominationFile.gateway.ts +++ b/apps/webapp/src/nomination-file/adapters/secondary/gateways/ApiNominationFile.gateway.ts @@ -48,6 +48,7 @@ export class ApiNominationFileGateway implements NominationFileGateway { targettedPosition: report.targettedPosition, comment: report.comment, rank: report.rank, + observers: report.observers, rules: report.rules, }; } @@ -64,6 +65,7 @@ export class ApiNominationFileGateway implements NominationFileGateway { transparency: item.transparency, grade: item.grade, targettedPosition: item.targettedPosition, + observersCount: item.observersCount, })); } } diff --git a/apps/webapp/src/nomination-file/adapters/secondary/gateways/FakeNominationFile.client.ts b/apps/webapp/src/nomination-file/adapters/secondary/gateways/FakeNominationFile.client.ts index 1ea8c41..f6ed1a7 100644 --- a/apps/webapp/src/nomination-file/adapters/secondary/gateways/FakeNominationFile.client.ts +++ b/apps/webapp/src/nomination-file/adapters/secondary/gateways/FakeNominationFile.client.ts @@ -2,11 +2,13 @@ import { ReportUpdateDto } from "api-sdk/generated/structures/ReportUpdateDto"; import { NominationFile, ReportListingVM, + ReportListItemVM, ReportRetrievalVM, rulesTuple, } from "shared-models"; import { NominationFileApiClient } from "../../../core-logic/gateways/NominationFileApi.client"; -import { FakeNominationFileFromApi } from "./FakeNominationFile.gateway"; + +export type FakeNominationFileFromApi = ReportRetrievalVM & ReportListItemVM; export class FakeNominationFileApiClient implements NominationFileApiClient { nominationFiles: Record = {}; @@ -74,6 +76,7 @@ export class FakeNominationFileApiClient implements NominationFileApiClient { async retrieveNominationFile(id: string): Promise { const fullReport = this.nominationFiles[id]; + if (!fullReport) return null; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { reporterName, ...report } = fullReport; @@ -91,6 +94,7 @@ export class FakeNominationFileApiClient implements NominationFileApiClient { transparency: report.transparency, grade: report.grade, targettedPosition: report.targettedPosition, + observersCount: report.observersCount, })); return { diff --git a/apps/webapp/src/nomination-file/adapters/secondary/gateways/FakeNominationFile.gateway.ts b/apps/webapp/src/nomination-file/adapters/secondary/gateways/FakeNominationFile.gateway.ts index d63f0e8..7fa0f0d 100644 --- a/apps/webapp/src/nomination-file/adapters/secondary/gateways/FakeNominationFile.gateway.ts +++ b/apps/webapp/src/nomination-file/adapters/secondary/gateways/FakeNominationFile.gateway.ts @@ -1,23 +1,27 @@ import { + Magistrat, NominationFile, ReportListItemVM, ReportRetrievalVM, + Transparency, } from "shared-models"; import { NominationFileGateway, UpdateNominationFileParams, } from "../../../core-logic/gateways/NominationFile.gateway"; -import { NominationFileListItem } from "../../../store/appState"; +import { + NominationFileListItem, + NominationFileSM, +} from "../../../store/appState"; -export type FakeNominationFileFromApi = ReportRetrievalVM & - Pick; +export type FakeNominationFileFromApi = ReportRetrievalVM | ReportListItemVM; export class FakeNominationFileGateway implements NominationFileGateway { private nominationFiles: Record = {}; private lastNominationFileId: string | null = null; async list(): Promise { - return Object.values(this.nominationFiles).map( + return (Object.values(this.nominationFiles) as ReportListItemVM[]).map( ({ id, name, @@ -28,6 +32,7 @@ export class FakeNominationFileGateway implements NominationFileGateway { transparency, grade, targettedPosition, + observersCount, }) => ({ id, name, @@ -38,6 +43,7 @@ export class FakeNominationFileGateway implements NominationFileGateway { transparency, grade, targettedPosition, + observersCount, }), ); } @@ -57,14 +63,15 @@ export class FakeNominationFileGateway implements NominationFileGateway { throw new Error( "You should set the nomination id for the fake to do the update", ); - if (this.nominationFiles[this.lastNominationFileId]) { - Object.entries( - this.nominationFiles[this.lastNominationFileId]!.rules, - ).forEach(([ruleGroup, ruleEntry]) => { + const nominationFile = this.nominationFiles[this.lastNominationFileId]; + + if (nominationFile) { + if (!("comment" in nominationFile)) + throw new Error("Fake nomination file should be a of type retrieval"); + + Object.entries(nominationFile.rules).forEach(([ruleGroup, ruleEntry]) => { Object.entries(ruleEntry).forEach(([ruleName, rule]) => { if (rule.id === ruleId) { - const nominationFile = - this.nominationFiles[this.lastNominationFileId!]!; // It looks like Redux makes some nested attributes read-only, // so we need to create a new object this.nominationFiles[this.lastNominationFileId!] = { @@ -95,10 +102,29 @@ export class FakeNominationFileGateway implements NominationFileGateway { } } - async retrieveNominationFile(id: string) { + async retrieveNominationFile(id: string): Promise { const nominationFile = this.nominationFiles[id]; if (!nominationFile) throw new Error("Nomination case not found"); - return nominationFile; + if (!("comment" in nominationFile)) + throw new Error("Fake nomination file should be a of type retrieval"); + + return { + id: nominationFile.id, + name: nominationFile.name, + biography: nominationFile.biography, + dueDate: nominationFile.dueDate, + birthDate: nominationFile.birthDate, + state: nominationFile.state as NominationFile.ReportState, + formation: nominationFile.formation as Magistrat.Formation, + transparency: nominationFile.transparency as Transparency, + grade: nominationFile.grade as Magistrat.Grade, + currentPosition: nominationFile.currentPosition, + targettedPosition: nominationFile.targettedPosition, + comment: nominationFile.comment, + rank: nominationFile.rank, + observers: nominationFile.observers, + rules: nominationFile.rules, + }; } addNominationFile(aNomination: FakeNominationFileFromApi) { diff --git a/apps/webapp/src/nomination-file/core-logic/builders/NominationFile.builder.ts b/apps/webapp/src/nomination-file/core-logic/builders/NominationFile.builder.ts index 3461de5..cc0d535 100644 --- a/apps/webapp/src/nomination-file/core-logic/builders/NominationFile.builder.ts +++ b/apps/webapp/src/nomination-file/core-logic/builders/NominationFile.builder.ts @@ -5,8 +5,7 @@ import { Transparency, } from "shared-models"; import { DateOnly } from "../../../shared-kernel/core-logic/models/date-only"; -import { NominationFileSM } from "../../store/appState"; -import { FakeNominationFileFromApi } from "../../adapters/secondary/gateways/FakeNominationFile.gateway"; +import { NominationFileListItem, NominationFileSM } from "../../store/appState"; export class NominationFileBuilder { private id: string; @@ -24,6 +23,7 @@ export class NominationFileBuilder { private targettedPosition: string; private rank: string; private comment: string | null; + private observers: string[] | null; constructor() { this.id = "nomination-file-id"; @@ -40,6 +40,7 @@ export class NominationFileBuilder { this.targettedPosition = "PG TJ Marseille"; this.rank = "(2 sur une liste de 3)"; this.comment = "Some comment"; + this.observers = ["observer 1", "observer 2"]; this.rules = rulesTuple.reduce( (acc, [ruleGroup, ruleName]) => { return { @@ -111,6 +112,10 @@ export class NominationFileBuilder { this.comment = comment; return this; } + withObservers(observers: string[] | null) { + this.observers = observers; + return this; + } withTransferTimeValidated(transferTime: boolean) { this.rules.management.TRANSFER_TIME.validated = transferTime; @@ -170,11 +175,24 @@ export class NominationFileBuilder { return this; } - build(): FakeNominationFileFromApi { + buildListVM(): NominationFileListItem { return { id: this.id, name: this.name, reporterName: this.reporterName, + dueDate: this.dueDate?.toStoreModel() ?? null, + state: this.state, + formation: this.formation, + transparency: this.transparency, + grade: this.grade, + targettedPosition: this.targettedPosition, + observersCount: this.observers?.length ?? 0, + }; + } + buildRetrieveVM(): NominationFileSM { + return { + id: this.id, + name: this.name, biography: this.biography, dueDate: this.dueDate?.toStoreModel() ?? null, birthDate: this.birthDate.toStoreModel(), @@ -186,7 +204,7 @@ export class NominationFileBuilder { targettedPosition: this.targettedPosition, rank: this.rank, comment: this.comment, - + observers: this.observers, rules: this.rules, }; } diff --git a/apps/webapp/src/nomination-file/core-logic/builders/NominationFileVM.builder.ts b/apps/webapp/src/nomination-file/core-logic/builders/NominationFileVM.builder.ts index 025e11f..735f17b 100644 --- a/apps/webapp/src/nomination-file/core-logic/builders/NominationFileVM.builder.ts +++ b/apps/webapp/src/nomination-file/core-logic/builders/NominationFileVM.builder.ts @@ -26,6 +26,7 @@ export class NominationFileBuilderVM { private targettedPosition: string; private comment: string | null; private rank: string; + private observers: NominationFileVM["observers"]; constructor() { this.id = "nomination-file-id"; @@ -41,6 +42,10 @@ export class NominationFileBuilderVM { this.targettedPosition = "targetted position"; this.comment = "Some comment"; this.rank = "(3 sur une liste de 3)"; + this.observers = [ + ["observer 1"], + ["observer 2", "VPI TJ Rennes", "(1 sur une liste de 2"], + ]; this.rulesChecked = rulesTuple.reduce( (acc, [ruleGroup, ruleName]) => { return { @@ -118,6 +123,10 @@ export class NominationFileBuilderVM { this.rank = rank; return this; } + withObservers(observers: NominationFileVM["observers"]) { + this.observers = observers; + return this; + } withTransferTimeChecked(transferTime: boolean) { this.rulesChecked.management.TRANSFER_TIME.checked = transferTime; @@ -199,6 +208,7 @@ export class NominationFileBuilderVM { targettedPosition: this.targettedPosition, comment: this.comment, rank: this.rank, + observers: this.observers, rulesChecked: this.rulesChecked, }; } @@ -222,6 +232,11 @@ export class NominationFileBuilderVM { .withRank(nominationFileStoreModel.rank) .withState(nominationFileStoreModel.state) .withTargettedPosition(nominationFileStoreModel.targettedPosition) - .withTransparency(nominationFileStoreModel.transparency); + .withTransparency(nominationFileStoreModel.transparency) + .withObservers( + nominationFileStoreModel.observers?.map( + (o) => o.split("\n") as [string, ...string[]], + ) || null, + ); } } diff --git a/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-file-listing/listNominationFile.use-case.spec.ts b/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-file-listing/listNominationFile.use-case.spec.ts index 7d25113..66475b1 100644 --- a/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-file-listing/listNominationFile.use-case.spec.ts +++ b/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-file-listing/listNominationFile.use-case.spec.ts @@ -57,6 +57,7 @@ describe("Nomination Files Listing", () => { transparency: aNominationFile.transparency, grade: aNominationFile.grade, targettedPosition: aNominationFile.targettedPosition, + observersCount: aNominationFile.observersCount, }, ], }, @@ -75,4 +76,4 @@ const aNominationFile = new NominationFileBuilder() .withName("Lucien Denan") .withReporterName(user.reporterName) .withDueDate(new DateOnly(2030, 10, 30)) - .build(); + .buildListVM(); diff --git a/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-file-retrieval/retrieveNominationFile.use-case.spec.ts b/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-file-retrieval/retrieveNominationFile.use-case.spec.ts index c059503..4ca53de 100644 --- a/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-file-retrieval/retrieveNominationFile.use-case.spec.ts +++ b/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-file-retrieval/retrieveNominationFile.use-case.spec.ts @@ -48,8 +48,8 @@ describe("Retrieve Nomination Case", () => { }); }); -const aNomination = new NominationFileBuilder().build(); +const aNomination = new NominationFileBuilder().buildRetrieveVM(); const anotherNomination = new NominationFileBuilder() .withId("another-nomination-file-id") - .build(); + .buildRetrieveVM(); diff --git a/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-file-update/updateNominationFile.use-case.spec.ts b/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-file-update/updateNominationFile.use-case.spec.ts index 026ac09..737b44f 100644 --- a/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-file-update/updateNominationFile.use-case.spec.ts +++ b/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-file-update/updateNominationFile.use-case.spec.ts @@ -67,4 +67,4 @@ const aNomination = new NominationFileBuilder() .withState(NominationFile.ReportState.NEW) .withBiography("John Doe's biography") .withComment("Some comment") - .build(); + .buildRetrieveVM(); diff --git a/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-rule-update/updateNominationRule.use-case.spec.ts b/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-rule-update/updateNominationRule.use-case.spec.ts index de62141..edd085d 100644 --- a/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-rule-update/updateNominationRule.use-case.spec.ts +++ b/apps/webapp/src/nomination-file/core-logic/use-cases/nomination-rule-update/updateNominationRule.use-case.spec.ts @@ -7,14 +7,14 @@ import { updateNominationRule } from "./updateNominationRule.use-case"; describe("Nomination Rule Update", () => { let store: ReduxStore; - let nominationCaseGateway: FakeNominationFileGateway; + let nominationFileGateway: FakeNominationFileGateway; let initialState: AppState; beforeEach(() => { - nominationCaseGateway = new FakeNominationFileGateway(); + nominationFileGateway = new FakeNominationFileGateway(); store = initReduxStore( { - nominationFileGateway: nominationCaseGateway, + nominationFileGateway, }, {}, {}, @@ -23,7 +23,7 @@ describe("Nomination Rule Update", () => { }); it("switch the transfer time rule from unvalidated to validated", async () => { - nominationCaseGateway.addNominationFile(aNomination); + nominationFileGateway.addNominationFile(aNomination); store.dispatch(retrieveNominationFile.fulfilled(aNomination, "", "")); await store.dispatch( updateNominationRule({ @@ -57,4 +57,4 @@ describe("Nomination Rule Update", () => { const aNomination = new NominationFileBuilder() .withTransferTimeValidated(false) - .build(); + .buildRetrieveVM(); diff --git a/apps/webapp/src/nomination-file/core-logic/view-models/NominationFileVM.ts b/apps/webapp/src/nomination-file/core-logic/view-models/NominationFileVM.ts index e8cddc2..7cb3a94 100644 --- a/apps/webapp/src/nomination-file/core-logic/view-models/NominationFileVM.ts +++ b/apps/webapp/src/nomination-file/core-logic/view-models/NominationFileVM.ts @@ -96,6 +96,7 @@ export class NominationFileVM { public targettedPosition: string, public comment: string | null, public rank: string, + public observers: [string, ...string[]][] | null, public rulesChecked: { [NominationFile.RuleGroup.MANAGEMENT]: Record< diff --git a/apps/webapp/src/nomination-file/store/appState.ts b/apps/webapp/src/nomination-file/store/appState.ts index d48d827..66cd7b7 100644 --- a/apps/webapp/src/nomination-file/store/appState.ts +++ b/apps/webapp/src/nomination-file/store/appState.ts @@ -16,8 +16,9 @@ export interface NominationFileSM { grade: Magistrat.Grade; currentPosition: string; targettedPosition: string; - rank: string; comment: string | null; + rank: string; + observers: string[] | null; rules: NominationFile.Rules; } export type NominationFileListItem = Pick< @@ -30,7 +31,7 @@ export type NominationFileListItem = Pick< | "transparency" | "grade" | "targettedPosition" -> & { reporterName: string | null }; +> & { reporterName: string | null; observersCount: number }; export interface AppState { nominationFileOverview: { byIds: Record | null }; diff --git a/apps/webapp/src/router/AppRouter.spec.tsx b/apps/webapp/src/router/AppRouter.spec.tsx index 7632f95..90327d0 100644 --- a/apps/webapp/src/router/AppRouter.spec.tsx +++ b/apps/webapp/src/router/AppRouter.spec.tsx @@ -1,4 +1,3 @@ -import "@testing-library/jest-dom"; import { act, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Provider } from "react-redux"; @@ -6,7 +5,6 @@ import { FakeAuthenticationGateway } from "../authentication/adapters/secondary/ import { authenticate } from "../authentication/core-logic/use-cases/authentication/authenticate"; import { NominationFileBuilder } from "../nomination-file/core-logic/builders/NominationFile.builder"; import { retrieveNominationFile } from "../nomination-file/core-logic/use-cases/nomination-file-retrieval/retrieveNominationFile.use-case"; -import { NominationFileSM } from "../nomination-file/store/appState"; import { initReduxStore, ReduxStore, @@ -126,11 +124,13 @@ describe("App Router Component", () => { renderAppRouter(); act(() => { givenAnAuthenticatedUser(); - store.dispatch(retrieveNominationFile.fulfilled(aNomination, "", "")); + store.dispatch( + retrieveNominationFile.fulfilled(aNominationRetrieved, "", ""), + ); }); act(() => { - routerProvider.gotToNominationFileOverview(aNomination.id); + routerProvider.gotToNominationFileOverview(aNominationRetrieved.id); }); expect(await screen.findByText("Mes rapports")).toHaveStyle({ @@ -185,4 +185,4 @@ describe("App Router Component", () => { } }); -const aNomination: NominationFileSM = new NominationFileBuilder().build(); +const aNominationRetrieved = new NominationFileBuilder().buildRetrieveVM(); diff --git a/apps/webapp/tsconfig.app.json b/apps/webapp/tsconfig.app.json index 2aac3e1..2972387 100644 --- a/apps/webapp/tsconfig.app.json +++ b/apps/webapp/tsconfig.app.json @@ -21,7 +21,7 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "types": ["vitest/globals"] + "types": ["vitest/globals", "@testing-library/jest-dom/vitest"] }, "include": ["src"], "references": [{ "path": "../../packages/shared-models" }] diff --git a/apps/webapp/vitest.config.ts b/apps/webapp/vitest.config.ts index 2e7b9ba..f6710a9 100644 --- a/apps/webapp/vitest.config.ts +++ b/apps/webapp/vitest.config.ts @@ -7,6 +7,6 @@ export default defineConfig({ test: { globals: true, environment: "jsdom", - setupFiles: ["@testing-library/jest-dom"], + setupFiles: ["@testing-library/jest-dom", "./vitest/vitest.setup.ts"], }, }); diff --git a/apps/webapp/vitest/vitest.setup.ts b/apps/webapp/vitest/vitest.setup.ts new file mode 100644 index 0000000..f149f27 --- /dev/null +++ b/apps/webapp/vitest/vitest.setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/packages/shared-models/models/report-retrieval-vm.ts b/packages/shared-models/models/report-retrieval-vm.ts index 43633af..71f740f 100644 --- a/packages/shared-models/models/report-retrieval-vm.ts +++ b/packages/shared-models/models/report-retrieval-vm.ts @@ -17,5 +17,6 @@ export interface ReportRetrievalVM { targettedPosition: string; comment: string | null; rank: string; + observers: string[] | null; rules: NominationFile.Rules; } diff --git a/packages/shared-models/models/reports-listing-vm.ts b/packages/shared-models/models/reports-listing-vm.ts index 0b95a06..e90da81 100644 --- a/packages/shared-models/models/reports-listing-vm.ts +++ b/packages/shared-models/models/reports-listing-vm.ts @@ -13,6 +13,7 @@ export interface ReportListItemVM { transparency: Transparency; grade: Magistrat.Grade; targettedPosition: string; + observersCount: number; } export interface ReportListingVM {