Skip to content

Commit 83329ca

Browse files
committed
feat(prélèvements): ajoute un filtre par laboratoire
1 parent 423d24b commit 83329ca

File tree

11 files changed

+137
-67
lines changed

11 files changed

+137
-67
lines changed

frontend/src/components/FilterTags/FiltersTags.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { MatrixKindLabels } from 'maestro-shared/referential/Matrix/MatrixKind';
88
import { MatrixLabels } from 'maestro-shared/referential/Matrix/MatrixLabels';
99
import { Regions } from 'maestro-shared/referential/Region';
1010
import { Pagination } from 'maestro-shared/schema/commons/Pagination';
11+
import { Laboratory } from 'maestro-shared/schema/Laboratory/Laboratory';
1112
import {
1213
ContextLabels,
1314
ProgrammingPlanContext
@@ -47,6 +48,7 @@ interface Props {
4748
onChange: (filters: Partial<FilterableType>) => void;
4849
samplers?: UserRefined[];
4950
programmingPlans?: ProgrammingPlanChecked[];
51+
laboratories?: Pick<Laboratory, 'id' | 'name'>[];
5052
}
5153

5254
const tagProps = {
@@ -158,6 +160,11 @@ const filtersConfig = {
158160
prop: 'domain',
159161
getLabel: (value) => ProgrammingPlanDomainLabels[value]
160162
},
163+
laboratoryId: {
164+
prop: 'laboratoryId',
165+
getLabel: (value, { laboratories }) =>
166+
laboratories?.find(({ id, name }) => id === value)?.name ?? ''
167+
},
161168
programmingPlanIds: {
162169
prop: 'programmingPlanIds',
163170
getComponent: (value, onChange, { programmingPlans }) => (
@@ -204,6 +211,7 @@ const filtersConfig = {
204211
value: NonNullable<FilterableType[key]>,
205212
data: {
206213
sampler?: UserRefined;
214+
laboratories?: Props['laboratories'];
207215
}
208216
) => string | null;
209217
getComponent?: never;
@@ -226,7 +234,8 @@ const FiltersTags = ({
226234
filters,
227235
onChange,
228236
samplers,
229-
programmingPlans
237+
programmingPlans,
238+
laboratories
230239
}: Props) => {
231240
const { hasNationalView } = useAuthentication();
232241
const sampler = useMemo(
@@ -272,7 +281,8 @@ const FiltersTags = ({
272281
} else {
273282
// @ts-expect-error TS2345 il est perdu
274283
const label = conf.getLabel(value, {
275-
sampler
284+
sampler,
285+
laboratories
276286
});
277287
if (label) {
278288
return (
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { cx } from '@codegouvfr/react-dsfr/fr/cx';
2+
import Select from '@codegouvfr/react-dsfr/Select';
3+
import { sortBy } from 'lodash-es';
4+
import { SubstanceKind } from 'maestro-shared/schema/Substance/SubstanceKind';
5+
import { useContext } from 'react';
6+
import { ApiClientContext } from '../../services/apiClient';
7+
8+
interface Props {
9+
programmingPlanId: string | undefined;
10+
substanceKind?: SubstanceKind;
11+
laboratoryId?: string | null;
12+
onSelect: (laboratoryId?: string) => void;
13+
readonly?: boolean;
14+
}
15+
16+
const LaboratorySelect = ({
17+
programmingPlanId,
18+
substanceKind,
19+
laboratoryId,
20+
onSelect,
21+
readonly
22+
}: Props) => {
23+
const apiClient = useContext(ApiClientContext);
24+
25+
const { data: laboratories } = apiClient.useFindLaboratoriesQuery({
26+
programmingPlanId,
27+
substanceKind
28+
});
29+
30+
return (
31+
<Select
32+
label="Laboratoire"
33+
nativeSelectProps={{
34+
value: laboratoryId ?? '',
35+
autoFocus: true,
36+
onChange: (e) => onSelect(e.target.value || undefined)
37+
}}
38+
className={cx('fr-mb-0')}
39+
disabled={readonly}
40+
>
41+
<option value="" disabled>
42+
Sélectionner un laboratoire
43+
</option>
44+
{sortBy(laboratories ?? [], 'name').map((laboratory) => (
45+
<option key={laboratory.id} value={laboratory.id}>
46+
{laboratory.name}
47+
</option>
48+
))}
49+
</Select>
50+
);
51+
};
52+
53+
export default LaboratorySelect;

frontend/src/components/LocalPrescription/LocalPrescriptionSubstanceKindsLaboratories/LocalPrescriptionSubstanceKindsLaboratories.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import {
33
SubstanceKindLaboratory,
44
SubstanceKindLaboratorySort
55
} from 'maestro-shared/schema/LocalPrescription/LocalPrescriptionSubstanceKindLaboratory';
6+
import { SubstanceKindLabels } from 'maestro-shared/schema/Substance/SubstanceKind';
67
import { forwardRef, useImperativeHandle, useState } from 'react';
7-
import SubstanceKindLaboratorySelect from './SubstanceKindLaboratorySelect';
8+
import LaboratorySelect from '../../LaboratorySelect/LaboratorySelect';
89

910
interface Props {
1011
programmingPlanId: string;
@@ -49,9 +50,13 @@ const LocalPrescriptionSubstanceKindsLaboratories = forwardRef<
4950
key={`substanceKindLaboratory_${substanceKindLaboratory.substanceKind}`}
5051
>
5152
{index > 0 && <hr className={cx('fr-mb-2w')} />}
52-
<SubstanceKindLaboratorySelect
53+
<div className={cx('fr-text--bold', 'fr-mb-2w')}>
54+
{SubstanceKindLabels[substanceKindLaboratory.substanceKind]}
55+
</div>
56+
<LaboratorySelect
5357
programmingPlanId={programmingPlanId}
54-
substanceKindLaboratory={substanceKindLaboratory}
58+
substanceKind={substanceKindLaboratory.substanceKind}
59+
laboratoryId={substanceKindLaboratory.laboratoryId}
5560
onSelect={(laboratoryId) =>
5661
setSubstanceKindsLaboratories(
5762
substanceKindsLaboratories.map((sl) =>

frontend/src/components/LocalPrescription/LocalPrescriptionSubstanceKindsLaboratories/SubstanceKindLaboratorySelect.tsx

Lines changed: 0 additions & 57 deletions
This file was deleted.

frontend/src/views/SampleListView/SampleListView.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ const SampleListView = () => {
8080
[programmingPlans]
8181
);
8282

83+
const { data: laboratories } = apiClient.useFindLaboratoriesQuery({
84+
programmingPlanId: programmingPlan?.id
85+
});
86+
8387
const [isFilterExpanded, setIsFilterExpanded] = useState(false);
8488

8589
useEffect(() => {
@@ -109,6 +113,7 @@ const SampleListView = () => {
109113
compliance:
110114
SampleCompliance.safeParse(searchParams.get('compliance')).data ??
111115
undefined,
116+
laboratoryId: searchParams.get('laboratoryId'),
112117
withAtLeastOneResidue:
113118
coerceToBooleanNullish().safeParse(
114119
searchParams.get('withAtLeastOneResidue')
@@ -247,6 +252,7 @@ const SampleListView = () => {
247252
year={Number(year)}
248253
filters={findSampleOptions}
249254
onChange={changeFilter}
255+
programmingPlanId={programmingPlan.id}
250256
/>
251257
)}
252258
</div>
@@ -258,6 +264,7 @@ const SampleListView = () => {
258264
programmingPlans={programmingPlans}
259265
samplers={samplers}
260266
onChange={changeFilter}
267+
laboratories={laboratories}
261268
/>
262269
</div>
263270
</div>
@@ -281,6 +288,7 @@ const SampleListView = () => {
281288
year={Number(year)}
282289
filters={findSampleOptions}
283290
onChange={changeFilter}
291+
programmingPlanId={programmingPlan?.id}
284292
/>
285293
)}
286294
</div>
@@ -289,6 +297,7 @@ const SampleListView = () => {
289297
filters={findSampleOptions}
290298
programmingPlans={programmingPlans}
291299
samplers={samplers}
300+
laboratories={laboratories}
292301
onChange={changeFilter}
293302
/>
294303
</div>

frontend/src/views/SampleListView/SampleSecondaryFilters.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,22 @@ import {
1616
import { useAuthentication } from 'src/hooks/useAuthentication';
1717
import { z } from 'zod';
1818
import { DepartmentsSelect } from '../../components/DepartmentsSelect/DepartmentsSelect';
19+
import LaboratorySelect from '../../components/LaboratorySelect/LaboratorySelect';
1920
import { RegionsFilter } from '../../components/RegionsFilter/RegionsFilter';
2021

2122
interface Props {
2223
year: number;
2324
filters: Partial<FindSampleOptions>;
25+
programmingPlanId: string | undefined;
2426
onChange: (filters: Partial<FindSampleOptions>) => void;
2527
}
2628

27-
const SampleSecondaryFilters = ({ year, filters, onChange }: Props) => {
29+
const SampleSecondaryFilters = ({
30+
year,
31+
filters,
32+
programmingPlanId,
33+
onChange
34+
}: Props) => {
2835
const { hasNationalView, hasRegionalView } = useAuthentication();
2936

3037
return (
@@ -121,6 +128,17 @@ const SampleSecondaryFilters = ({ year, filters, onChange }: Props) => {
121128
))}
122129
</Select>
123130
</div>
131+
<div className={cx('fr-col-12', 'fr-col-md-6', 'fr-col-lg-3')}>
132+
<LaboratorySelect
133+
programmingPlanId={programmingPlanId}
134+
laboratoryId={filters.laboratoryId}
135+
onSelect={(laboratoryId) =>
136+
onChange({
137+
laboratoryId
138+
})
139+
}
140+
/>
141+
</div>
124142
<div className={cx('fr-col-12', 'fr-col-md-6', 'fr-col-lg-3')}>
125143
<ToggleSwitch
126144
label="Avec au moins un résidu"

server/repositories/laboratoryRepository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const findMany = async (
3333
'laboratoryAgreements.laboratoryId',
3434
'laboratories.id'
3535
)
36-
.distinct()
36+
.distinctOn('laboratories.id')
3737
.selectAll();
3838

3939
for (const option of FindLaboratoryOptions.keyof().options) {

server/repositories/sampleItemRepository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { knexInstance as db } from './db';
77
import { kysely } from './kysely';
88
import { KyselyMaestro } from './kysely.type';
99

10-
const sampleItemsTable = 'sample_items';
10+
export const sampleItemsTable = 'sample_items';
1111

1212
export const SampleItems = (transaction = db) =>
1313
transaction<PartialSampleItem>(sampleItemsTable);

server/repositories/sampleRepository.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MatrixKind } from 'maestro-shared/referential/Matrix/MatrixKind';
33
import { ResultKind } from 'maestro-shared/schema/Analysis/Residue/ResultKind';
44
import { genPartialAnalysis } from 'maestro-shared/test/analysisFixtures';
55
import { genDocument } from 'maestro-shared/test/documentFixtures';
6+
import { LaboratoryFixture } from 'maestro-shared/test/laboratoryFixtures';
67
import {
78
Sample11Fixture,
89
Sample2Fixture
@@ -134,6 +135,21 @@ describe('findMany samples', async () => {
134135
expect(samples).not.toHaveLength(1);
135136
});
136137

138+
test('find with laboratories option', async () => {
139+
let samples = await sampleRepository.findMany({
140+
programmingPlanIds: toArray(Sample11Fixture.programmingPlanId),
141+
laboratoryId: '00000000-0000-0000-0000-000000000000'
142+
});
143+
expect(samples).toEqual([]);
144+
145+
samples = await sampleRepository.findMany({
146+
programmingPlanIds: toArray(Sample11Fixture.programmingPlanId),
147+
laboratoryId: LaboratoryFixture.id
148+
});
149+
expect(samples).toHaveLength(1);
150+
expect(samples[0].id).toBe(Sample11Fixture.id);
151+
});
152+
137153
test('find with withAtLeastOneResidue option', async () => {
138154
const document = genDocument({
139155
createdBy: Sampler1Fixture.id,

server/repositories/sampleRepository.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { companiesTable } from './companyRepository';
1313
import { knexInstance as db, knexInstance } from './db';
1414
import { kysely } from './kysely';
1515
import { KyselyMaestro } from './kysely.type';
16+
import { sampleItemsTable } from './sampleItemRepository';
1617
import { usersTable } from './userRepository';
1718

1819
export const samplesTable = 'samples';
@@ -120,7 +121,8 @@ const findRequest = (findOptions: FindSampleOptions) =>
120121
'compliance',
121122
'withAtLeastOneResidue',
122123
'programmingPlanIds',
123-
'kinds'
124+
'kinds',
125+
'laboratoryId'
124126
),
125127
(_) => isNil(_) || isArray(_)
126128
)
@@ -201,6 +203,19 @@ const findRequest = (findOptions: FindSampleOptions) =>
201203
.limit(1)
202204
);
203205
}
206+
if (findOptions.laboratoryId) {
207+
builder.whereExists((c) =>
208+
c
209+
.select(knexInstance.raw(1))
210+
.from(sampleItemsTable)
211+
.where(
212+
`${sampleItemsTable}.sampleId`,
213+
knexInstance.raw(`${samplesTable}.id`)
214+
)
215+
.where(`${sampleItemsTable}.laboratoryId`, findOptions.laboratoryId)
216+
.limit(1)
217+
);
218+
}
204219
});
205220

206221
const findMany = async (

0 commit comments

Comments
 (0)