diff --git a/dashboard/src/components/Card.js b/dashboard/src/components/Card.js
index 2c42f3f4b..60f4868f1 100644
--- a/dashboard/src/components/Card.js
+++ b/dashboard/src/components/Card.js
@@ -1,7 +1,9 @@
import React from 'react';
import HelpButtonAndModal from './HelpButtonAndModal';
-const Card = ({ title, count, unit, children, countId, dataTestId, help }) => {
+const Card = ({ title, count, unit, children, countId, dataTestId, help, onClick = null }) => {
+ const Component = !!onClick ? 'button' : 'div';
+ const props = !!onClick ? { onClick, type: 'button', name: 'card', className: 'button-cancel' } : {};
return (
<>
@@ -12,12 +14,12 @@ const Card = ({ title, count, unit, children, countId, dataTestId, help }) => {
)}
-
+
{count}
{!!unit && {unit}}
-
+
{children}
>
diff --git a/dashboard/src/scenes/report/components/ObservationsReport.js b/dashboard/src/scenes/report/components/ObservationsReport.js
index 2e6986bf5..da8f83d86 100644
--- a/dashboard/src/scenes/report/components/ObservationsReport.js
+++ b/dashboard/src/scenes/report/components/ObservationsReport.js
@@ -1,6 +1,6 @@
-import React, { useState } from 'react';
-import dayjs from 'dayjs';
+import { useState } from 'react';
import { useRecoilValue } from 'recoil';
+import { utils, writeFile } from 'xlsx';
import { ModalHeader, ModalBody, ModalContainer, ModalFooter } from '../../../components/tailwind/Modal';
import { FullScreenIcon } from '../../../assets/icons/FullScreenIcon';
import Table from '../../../components/table';
@@ -9,6 +9,9 @@ import DateBloc from '../../../components/DateBloc';
import CreateObservation from '../../../components/CreateObservation';
import Observation from '../../territory-observations/view';
import { territoriesState } from '../../../recoil/territory';
+import { dayjsInstance } from '../../../services/date';
+import { teamsState, usersState } from '../../../recoil/auth';
+import { customFieldsObsSelector } from '../../../recoil/territoryObservations';
export const ObservationsReport = ({ observations, period, selectedTeams }) => {
const [fullScreen, setFullScreen] = useState(false);
@@ -44,18 +47,70 @@ const ObservationsTable = ({ period, observations, selectedTeams }) => {
const [observationToEdit, setObservationToEdit] = useState({});
const [openObservationModaleKey, setOpenObservationModaleKey] = useState(0);
const territories = useRecoilValue(territoriesState);
+ const teams = useRecoilValue(teamsState);
+ const customFieldsObs = useRecoilValue(customFieldsObsSelector);
+ const users = useRecoilValue(usersState);
+
+ const exportXlsx = () => {
+ const wb = utils.book_new();
+ const formattedData = utils.json_to_sheet(
+ observations.map((observation) => {
+ return {
+ id: observation._id,
+ 'Territoire - Nom': territories.find((t) => t._id === observation.territory)?.name,
+ 'Observé le': dayjsInstance(observation.observedAt).format('YYYY-MM-DD HH:mm'),
+ Équipe: observation.team ? teams.find((t) => t._id === observation.team)?.name : '',
+ ...customFieldsObs.reduce((fields, field) => {
+ if (['date', 'date-with-time'].includes(field.type))
+ fields[field.label || field.name] = observation[field.name]
+ ? dayjsInstance(observation[field.name]).format(field.type === 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm')
+ : '';
+ else if (['boolean'].includes(field.type)) fields[field.label || field.name] = observation[field.name] ? 'Oui' : 'Non';
+ else if (['yes-no'].includes(field.type)) fields[field.label || field.name] = observation[field.name];
+ else if (Array.isArray(observation[field.name])) fields[field.label || field.name] = observation[field.name].join(', ');
+ else fields[field.label || field.name] = observation[field.name];
+ return fields;
+ }, {}),
+ 'Créée par': users.find((u) => u._id === observation.user)?.name,
+ 'Créée le': dayjsInstance(observation.createdAt).format('YYYY-MM-DD HH:mm'),
+ 'Mise à jour le': dayjsInstance(observation.updatedAt).format('YYYY-MM-DD HH:mm'),
+ };
+ })
+ );
+ utils.book_append_sheet(wb, formattedData, 'Observations de territoires');
+
+ utils.book_append_sheet(wb, utils.json_to_sheet(observations), 'Observations (données brutes)');
+ utils.book_append_sheet(wb, utils.json_to_sheet(territories), 'Territoires (données brutes)');
+ utils.book_append_sheet(wb, utils.json_to_sheet(selectedTeams), 'Filtres (équipes)');
+ const otherFilters = [
+ {
+ 'Période - début': period.startDate,
+ 'Période - fin': period.endDate,
+ },
+ ];
+ utils.book_append_sheet(wb, utils.json_to_sheet(otherFilters), 'Filtres (autres)');
+ writeFile(
+ wb,
+ `Compte rendu (${dayjsInstance(period.startDate).format('YYYY-MM-DD')} - ${dayjsInstance(period.endDate).format(
+ 'YYYY-MM-DD'
+ )}) - Observations de territoires (${observations.length}).xlsx`
+ );
+ };
return (
<>
Observations
+
}
+ dataTestId={dataTestId}
+ help={help?.(col.title.capitalize())}
+ onClick={col.onBlockClick ? col.onBlockClick : null}
+ />
))}
{customFieldsInStats.map((field) => {
diff --git a/dashboard/src/scenes/stats/ObservationsStats.js b/dashboard/src/scenes/stats/ObservationsStats.js
index b1bf94a45..d9c90af0a 100644
--- a/dashboard/src/scenes/stats/ObservationsStats.js
+++ b/dashboard/src/scenes/stats/ObservationsStats.js
@@ -1,8 +1,40 @@
-import React from 'react';
+import { useState } from 'react';
+import { useRecoilValue } from 'recoil';
+import { utils, writeFile } from 'xlsx';
import SelectCustom from '../../components/SelectCustom';
import CustomFieldsStats from './CustomFieldsStats';
+import { ModalBody, ModalContainer, ModalFooter, ModalHeader } from '../../components/tailwind/Modal';
+import { teamsState, usersState } from '../../recoil/auth';
+import TagTeam from '../../components/TagTeam';
+import Table from '../../components/table';
+import { dayjsInstance } from '../../services/date';
+import { customFieldsObsSelector } from '../../recoil/territoryObservations';
+import CreateObservation from '../../components/CreateObservation';
+import { filterData } from '../../components/Filters';
+import DateBloc from '../../components/DateBloc';
+import Observation from '../../scenes/territory-observations/view';
-const ObservationsStats = ({ territories, setSelectedTerritories, observations, customFieldsObs }) => {
+const ObservationsStats = ({ territories, setSelectedTerritories, observations, customFieldsObs, allFilters }) => {
+ const [obsModalOpened, setObsModalOpened] = useState(false);
+ const [sliceField, setSliceField] = useState(null);
+ const [sliceValue, setSliceValue] = useState(null);
+ const [slicedData, setSlicedData] = useState([]);
+
+ const onSliceClick = (newSlice, fieldName, observationsConcerned = observations) => {
+ const newSlicefield = customFieldsObs.find((f) => f.name === fieldName);
+ setSliceField(newSlicefield);
+ setSliceValue(newSlice);
+ const slicedData =
+ newSlicefield.type === 'boolean'
+ ? observationsConcerned.filter((p) => (newSlice === 'Non' ? !p[newSlicefield.field] : !!p[newSlicefield.field]))
+ : filterData(
+ observationsConcerned,
+ [{ ...newSlicefield, value: newSlice, type: newSlicefield.field === 'outOfActiveList' ? 'boolean' : newSlicefield.field }],
+ true
+ );
+ setSlicedData(slicedData);
+ setObsModalOpened(true);
+ };
return (
<>
Statistiques des observations de territoire
@@ -25,11 +57,16 @@ const ObservationsStats = ({ territories, setSelectedTerritories, observations,
{
+ setSlicedData(observations);
+ setObsModalOpened(true);
+ },
},
]}
help={(label) =>
@@ -37,6 +74,144 @@ const ObservationsStats = ({ territories, setSelectedTerritories, observations,
}
totalTitleForMultiChoice={Nombre d'observations concernées}
/>
+ {
+ setObsModalOpened(false);
+ }}
+ observations={slicedData}
+ sliceField={sliceField}
+ onAfterLeave={() => {
+ setSliceField(null);
+ setSliceValue(null);
+ setSlicedData([]);
+ }}
+ title={`${sliceField?.label ?? 'Observations de territoire'}${sliceValue ? ` : ${sliceValue}` : ''} (${slicedData.length})`}
+ territories={territories}
+ allFilters={allFilters}
+ />
+ >
+ );
+};
+
+const SelectedObsModal = ({ open, onClose, observations, territories, title, onAfterLeave, allFilters }) => {
+ const [observationToEdit, setObservationToEdit] = useState({});
+ const [openObservationModaleKey, setOpenObservationModaleKey] = useState(0);
+ const teams = useRecoilValue(teamsState);
+ const customFieldsObs = useRecoilValue(customFieldsObsSelector);
+ const users = useRecoilValue(usersState);
+
+ const exportXlsx = () => {
+ const wb = utils.book_new();
+ const formattedData = utils.json_to_sheet(
+ observations.map((observation) => {
+ return {
+ id: observation._id,
+ 'Territoire - Nom': territories.find((t) => t._id === observation.territory)?.name,
+ 'Observé le': dayjsInstance(observation.observedAt).format('YYYY-MM-DD HH:mm'),
+ Équipe: observation.team ? teams.find((t) => t._id === observation.team)?.name : '',
+ ...customFieldsObs.reduce((fields, field) => {
+ if (['date', 'date-with-time'].includes(field.type))
+ fields[field.label || field.name] = observation[field.name]
+ ? dayjsInstance(observation[field.name]).format(field.type === 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm')
+ : '';
+ else if (['boolean'].includes(field.type)) fields[field.label || field.name] = observation[field.name] ? 'Oui' : 'Non';
+ else if (['yes-no'].includes(field.type)) fields[field.label || field.name] = observation[field.name];
+ else if (Array.isArray(observation[field.name])) fields[field.label || field.name] = observation[field.name].join(', ');
+ else fields[field.label || field.name] = observation[field.name];
+ return fields;
+ }, {}),
+ 'Créée par': users.find((u) => u._id === observation.user)?.name,
+ 'Créée le': dayjsInstance(observation.createdAt).format('YYYY-MM-DD HH:mm'),
+ 'Mise à jour le': dayjsInstance(observation.updatedAt).format('YYYY-MM-DD HH:mm'),
+ };
+ })
+ );
+ utils.book_append_sheet(wb, formattedData, 'Observations de territoires');
+
+ utils.book_append_sheet(wb, utils.json_to_sheet(observations), 'Observations (données brutes)');
+ utils.book_append_sheet(wb, utils.json_to_sheet(territories), 'Territoires (données brutes)');
+ utils.book_append_sheet(wb, utils.json_to_sheet(allFilters.selectedTerritories), 'Filtres (territoires)');
+ utils.book_append_sheet(wb, utils.json_to_sheet(allFilters.selectedTeams), 'Filtres (équipes)');
+ const otherFilters = [
+ {
+ 'Période - début': allFilters.period.startDate,
+ 'Période - fin': allFilters.period.endDate,
+ },
+ ];
+ utils.book_append_sheet(wb, utils.json_to_sheet(otherFilters), 'Filtres (autres)');
+ writeFile(
+ wb,
+ `Statistiques (${dayjsInstance(allFilters.period.startDate).format('YYYY-MM-DD')} - ${dayjsInstance(allFilters.period.endDate).format(
+ 'YYYY-MM-DD'
+ )}) - ${title}.xlsx`
+ );
+ };
+
+ return (
+ <>
+
+
+ {title}{' '}
+
+
+ }
+ />
+
+
+
{
+ setObservationToEdit(obs);
+ setOpenObservationModaleKey((k) => k + 1);
+ }}
+ rowKey={'_id'}
+ columns={[
+ {
+ title: 'Date',
+ dataKey: 'observedAt',
+ render: (obs) => {
+ // anonymous comment migrated from `report.observations`
+ // have no time
+ // have no user assigned either
+ const time = dayjsInstance(obs.observedAt).format('D MMM HH:mm');
+ return (
+ <>
+
+ {time === '00:00' && !obs.user ? null : time}
+ >
+ );
+ },
+ },
+ { title: 'Territoire', dataKey: 'territory', render: (obs) => territories.find((t) => t._id === obs.territory)?.name },
+ { title: 'Observation', dataKey: 'entityKey', render: (obs) => , left: true },
+ {
+ title: 'Équipe en charge',
+ dataKey: 'team',
+ render: (obs) => ,
+ },
+ ]}
+ />
+
+
+
+
+
+
+
>
);
};
diff --git a/dashboard/src/scenes/stats/index.js b/dashboard/src/scenes/stats/index.js
index ba58b8ae3..aa4ea47a6 100644
--- a/dashboard/src/scenes/stats/index.js
+++ b/dashboard/src/scenes/stats/index.js
@@ -669,6 +669,13 @@ const Stats = () => {
setSelectedTerritories={setSelectedTerritories}
observations={observations}
customFieldsObs={customFieldsObs}
+ // `allFilters` is for debug purpose only
+ // TODO: remove when debugged
+ allFilters={{
+ selectedTerritories,
+ period,
+ selectedTeams,
+ }}
/>
)}
{activeTab === 'Comptes-rendus' && }