Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save housing precisions #1110

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
22265df
feat(frontend): add ConfirmationModalNext to replace the old confirma…
Falinor Jan 23, 2025
c6c3a92
feat(frontend): split the precision modal into two components
Falinor Jan 23, 2025
53806a1
feat(frontend): map the old precisions to the new format
Falinor Jan 23, 2025
471bda4
feat(frontend): immediately save precisions on modal close
Falinor Jan 23, 2025
f0f4f94
feat(frontend): only send the relevant payload when updating housing
Falinor Jan 23, 2025
47512fa
feat(server): link precisions to a housing
Falinor Jan 23, 2025
0b8a5f6
feat(server): add a script to migrate precisions
Falinor Jan 23, 2025
68ac8eb
feat(server): add PrecisionMissingError
Falinor Jan 23, 2025
8ba92da
feat: set tab when opening precisions; serialize and deserialize old …
Falinor Jan 23, 2025
036d022
refactor(server): simplify precisions migration script
Falinor Jan 24, 2025
1bd2e97
fix(server): ensure writing to the old and new precisions
Falinor Jan 24, 2025
6f8972d
fix(frontend): use only the well formed precisions
Falinor Jan 24, 2025
61fea15
feat: replace the hardcoded precision list by a service
Falinor Jan 24, 2025
07b9ccf
fix(server): fix precision and vacancy reason comparisons
Falinor Jan 24, 2025
2dbf8c7
fix(frontend): fix type error
Falinor Jan 24, 2025
312edc1
feat(frontend): add extra-large size to the PrecisionModal
Falinor Jan 24, 2025
6bb2fc7
test(frontend): add precision handlers
Falinor Jan 24, 2025
d0ce12a
fix: typo in existing precisions and vacancy reasons
Falinor Jan 24, 2025
192fc55
fix: send null values instead of empty strings
Falinor Jan 24, 2025
2000d40
fix: UI adjustments and add 'view more' button
loicguillois Jan 30, 2025
e4fd768
refactor(frontend): import PrecisionCategory; replace px by rem
Falinor Feb 3, 2025
f090e00
Merge pull request #1112 from MTES-MCT/retours-lucas-feat-save-housin…
Falinor Feb 3, 2025
6ddd374
fix(frontend): replace housing evolution checkboxes by radio buttons
Falinor Feb 3, 2025
d888117
feat(frontend): specify the precision category for housing evolutions
Falinor Feb 3, 2025
2c7498f
test(server): fix precision repository test
Falinor Feb 3, 2025
d477f25
fix(frontend): fix select opacity on Chrome; add an empty option
Falinor Feb 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/App.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@use "reset";
@use "colors";
@use "dsfr-fix";
@use "src/components/Map/housing-popup-overrides";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function HousingDetailsCardMobilisation({ housing, campaigns }: Props) {
<>Aucun dispositif indiqué</>
) : (
housing.precisions?.map((precision, index) => (
<Tag key={'precision_' + index} className="d-block fr-mb-1w">
<Tag key={'precision_' + index} className="d-block fr-m-1w">
{precision.startsWith('Dispositif')
? precision.split(OptionTreeSeparator).reverse()[0]
: precision
Expand All @@ -97,7 +97,7 @@ function HousingDetailsCardMobilisation({ housing, campaigns }: Props) {
housing.vacancyReasons?.map((vacancyReason, index) => (
<Tag
key={'vacancyReason_' + index}
className="d-block fr-mb-1w"
className="d-block fr-m-1w"
>
{vacancyReason.split(OptionTreeSeparator).reverse()[0]}
</Tag>
Expand Down
210 changes: 190 additions & 20 deletions frontend/src/components/HousingEdition/HousingEditionSideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@ import Grid from '@mui/material/Unstable_Grid2';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { fromJS } from 'immutable';
import fp from 'lodash/fp';
import { FormProvider, useController, useForm } from 'react-hook-form';
import { ElementOf } from 'ts-essentials';
import * as yup from 'yup';
import styles from './housing-edition.module.scss';

import {
HOUSING_STATUS_VALUES,
HousingStatus,
isPrecisionBlockingPointCategory,
isPrecisionEvolutionCategory,
isPrecisionMechanismCategory,
Occupancy,
OCCUPANCY_VALUES,
PRECISION_MECHANISM_CATEGORY_VALUES,
Precision,
PrecisionCategory
} from '@zerologementvacant/models';
import { Housing, HousingUpdate } from '../../models/Housing';
Expand All @@ -34,6 +39,11 @@ import AppTextInputNext from '../_app/AppTextInput/AppTextInputNext';
import { useCreateNoteByHousingMutation } from '../../services/note.service';
import { useUpdateHousingNextMutation } from '../../services/housing.service';
import { useNotification } from '../../hooks/useNotification';
import { toNewPrecision } from '../../models/Precision';
import createPrecisionModalNext from '../Precision/PrecisionModalNext';
import React, { useState } from 'react';
import { PrecisionTabId } from '../Precision/PrecisionTabs';
import { useFindPrecisionsQuery } from '../../services/precision.service';

interface HousingEditionSideMenuProps {
housing: Housing | null;
Expand All @@ -43,6 +53,7 @@ interface HousingEditionSideMenuProps {
}

const WIDTH = '700px';
const DISPLAY_TAGS = 6;

const schema = yup.object({
occupancy: yup
Expand All @@ -65,14 +76,20 @@ const schema = yup.object({

type FormSchema = yup.InferType<typeof schema>;

const precisionModal = createPrecisionModalNext();

function HousingEditionSideMenu(props: HousingEditionSideMenuProps) {
const { housing, expand, onClose } = props;
const [showAllMechanisms, setShowAllMechanisms] = useState(false);
const [showAllBlockingPoints, setShowAllBlockingPoints] = useState(false);
const [showAllEvolutions, setShowAllEvolutions] = useState(false);

const form = useForm<FormSchema>({
values: {
occupancy: props.housing?.occupancy ?? Occupancy.UNKNOWN,
occupancyIntended: props.housing?.occupancyIntended ?? Occupancy.UNKNOWN,
status: props.housing?.status ?? HousingStatus.NEVER_CONTACTED,
subStatus: props.housing?.subStatus ?? '',
subStatus: props.housing?.subStatus ?? null,
note: ''
},
mode: 'onSubmit',
Expand Down Expand Up @@ -105,6 +122,18 @@ function HousingEditionSideMenu(props: HousingEditionSideMenuProps) {
}
});

const { data } = useFindPrecisionsQuery();
const precisionOptions = data ?? [];
const precisions =
housing?.precisions?.length && precisionOptions.length
? housing.precisions
.concat(housing?.vacancyReasons ?? [])
// Only keep the well formed precisions and vacancy reasons
// like `Dispositifs > Dispositifs incitatifs > Réserve personnelle ou pour une autre personne`
.filter((precision) => precision.split(' > ').length === 3)
.map((precision) => toNewPrecision(precisionOptions, precision))
: [];

function submit() {
if (housing) {
const { note, ...payload } = form.getValues();
Expand All @@ -120,7 +149,8 @@ function HousingEditionSideMenu(props: HousingEditionSideMenuProps) {
occupancy: payload.occupancy as Occupancy,
occupancyIntended: payload.occupancyIntended as Occupancy | null,
status: payload.status as HousingStatus,
subStatus: payload.subStatus
subStatus: payload.subStatus,
precisions: precisions.map((p) => p.id)
});
}

Expand All @@ -136,6 +166,30 @@ function HousingEditionSideMenu(props: HousingEditionSideMenuProps) {
form.reset();
}

interface UseFilteredPrecisionsResult {
totalCount: number;
filteredItems: Precision[];
remainingCount: number;
}

function useFilteredPrecisions(
categoryFilter: (category: PrecisionCategory) => boolean,
showAll: boolean
): UseFilteredPrecisionsResult {
const allItems = precisions.filter((precision) =>
categoryFilter(precision.category)
);

return {
totalCount: allItems.length,
filteredItems: allItems.slice(
0,
showAll ? allItems.length : DISPLAY_TAGS
),
remainingCount: Math.max(0, allItems.length - DISPLAY_TAGS)
};
}

function OccupationTab(): ElementOf<TabsProps.Uncontrolled['tabs']> {
return {
content: (
Expand All @@ -160,18 +214,58 @@ function HousingEditionSideMenu(props: HousingEditionSideMenuProps) {
}

function MobilisationTab(): ElementOf<TabsProps.Uncontrolled['tabs']> {
// TODO: fetch `GET /precisions`
const precisions: ReadonlyArray<PrecisionCategory> = [];
const mechanisms = precisions.filter((precision) =>
PRECISION_MECHANISM_CATEGORY_VALUES.includes(precision)
const {
totalCount: totalMechanisms,
filteredItems: filteredMechanisms,
remainingCount: moreMechanisms
} = useFilteredPrecisions(isPrecisionMechanismCategory, showAllMechanisms);

const {
totalCount: totalBlockingPoints,
filteredItems: filteredBlockingPoints,
remainingCount: moreBlockingPoints
} = useFilteredPrecisions(
isPrecisionBlockingPointCategory,
showAllBlockingPoints
);

const {
totalCount: totalEvolutions,
filteredItems: filteredEvolutions,
remainingCount: moreEvolutions
} = useFilteredPrecisions(isPrecisionEvolutionCategory, showAllEvolutions);

interface ToggleShowAllProps {
setShowAll: React.Dispatch<React.SetStateAction<boolean>>;
}

function toggleShowAll({ setShowAll }: ToggleShowAllProps): void {
setShowAll((prev) => !prev);
}

const [tab, setTab] = useState<PrecisionTabId>('dispositifs');

const { field: statusField, fieldState: statusFieldState } =
useController<FormSchema>({
name: 'status',
control: form.control
});

// Immediately save the selected precisions
function savePrecisions(precisions: Precision[]) {
if (housing) {
updateHousing({
...housing,
occupancy: housing.occupancy as Occupancy,
occupancyIntended: housing.occupancyIntended as Occupancy,
precisions: precisions.map((p) => p.id)
});
}
}

const subStatusDisabled =
getSubStatusOptions(statusField.value as HousingStatus) === undefined;

return {
content: (
<Grid component="section" container sx={{ rowGap: 2 }}>
Expand All @@ -196,10 +290,7 @@ function HousingEditionSideMenu(props: HousingEditionSideMenuProps) {
}}
/>
<AppSelectNext
disabled={
getSubStatusOptions(statusField.value as HousingStatus) ===
undefined
}
disabled={subStatusDisabled}
label="Sous-statut de suivi"
name="subStatus"
multiple={false}
Expand Down Expand Up @@ -227,16 +318,40 @@ function HousingEditionSideMenu(props: HousingEditionSideMenuProps) {
fontWeight: 700
}}
>
Dispositifs ({mechanisms.length})
Dispositifs ({totalMechanisms})
</Typography>
<Button priority="secondary" title="Modifier les dispositifs">
<Button
priority="secondary"
title="Modifier les dispositifs"
onClick={() => {
setTab('dispositifs');
precisionModal.open();
}}
>
Modifier
</Button>
</Grid>
<Grid>
<Tag>Ma Prime Renov’</Tag>
<Tag>Aides aux travaux</Tag>
{filteredMechanisms.map((precision) => (
<Tag key={precision.id} className={styles.tag}>
{precision.label}
</Tag>
))}
</Grid>
{moreMechanisms > 0 && (
<Grid component="footer">
<Button
priority="tertiary"
onClick={() =>
toggleShowAll({ setShowAll: setShowAllMechanisms })
}
>
{showAllMechanisms
? 'Afficher moins'
: `Afficher plus (${moreMechanisms})`}
</Button>
</Grid>
)}
</Grid>

<Grid
Expand All @@ -257,22 +372,46 @@ function HousingEditionSideMenu(props: HousingEditionSideMenuProps) {
fontWeight: 700
}}
>
Points de blocages (0)
Points de blocages ({totalBlockingPoints})
</Typography>
<Button
priority="secondary"
title="Modifier les points de blocage"
onClick={() => {
setTab('points-de-blocage');
precisionModal.open();
}}
>
Modifier
</Button>
</Grid>
<Grid>Badges</Grid>
<Grid>
{filteredBlockingPoints.map((precision) => (
<Tag key={precision.id} className={styles.tag}>
{precision.label}
</Tag>
))}
</Grid>
{moreBlockingPoints > 0 && (
<Grid component="footer">
<Button
priority="tertiary"
onClick={() =>
toggleShowAll({ setShowAll: setShowAllBlockingPoints })
}
>
{showAllBlockingPoints
? 'Afficher moins'
: `Afficher plus (${moreBlockingPoints})`}
</Button>
</Grid>
)}
</Grid>

<Grid
component="article"
container
sx={{ alignItems: 'center', columnGap: 2 }}
sx={{ alignItems: 'center', columnGap: 2, rowGap: 1 }}
xs={12}
>
<Grid
Expand All @@ -283,19 +422,50 @@ function HousingEditionSideMenu(props: HousingEditionSideMenuProps) {
component="h3"
sx={{ fontSize: '1.125rem', fontWeight: 700 }}
>
Évolutions du logement (1)
Évolutions du logement ({totalEvolutions})
</Typography>
<Button
priority="secondary"
title="Modifier les évolutions du logement"
onClick={() => {
setTab('evolutions');
precisionModal.open();
}}
>
Modifier
</Button>
</Grid>
<Grid>
<Tag>Travaux : en cours</Tag>
{filteredEvolutions.map((precision) => (
<Tag key={precision.id} className={styles.tag}>
{fp.startCase(precision.category.replace('-', ' '))} :&nbsp;
{precision.label.toLowerCase()}
</Tag>
))}
</Grid>
{moreEvolutions > 0 && (
<Grid component="footer">
<Button
priority="tertiary"
onClick={() =>
toggleShowAll({ setShowAll: setShowAllEvolutions })
}
>
{showAllEvolutions
? 'Afficher moins'
: `Afficher plus (${moreEvolutions})`}
</Button>
</Grid>
)}
</Grid>

<precisionModal.Component
tab={tab}
options={precisionOptions}
value={precisions}
onSubmit={savePrecisions}
onTabChange={setTab}
/>
</Grid>
),
label: 'Mobilisation'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useState } from 'react';
import { useId, useRef, useState } from 'react';
import classNames from 'classnames';
import { useOutsideClick } from '../../hooks/useOutsideClick';
import { SelectOption } from '../../models/SelectOption';
Expand Down Expand Up @@ -34,16 +34,21 @@ const HousingStatusSelect = ({
setShowOptions(false);
};

const inputId = useId();

return (
<div className="select-single-input" ref={wrapperRef}>
<div
className={classNames({
[`fr-select-group--${messageType}`]: messageType
})}
>
<label className="fr-label">Statut de suivi</label>
<label className="fr-label" htmlFor={inputId}>
Statut de suivi
</label>
<button
className="fr-select"
id={inputId}
title={showOptions ? 'Masquer les options' : 'Afficher les options'}
onClick={() => setShowOptions(!showOptions)}
>
Expand Down
Loading
Loading