diff --git a/dashboard/src/components/Card.js b/dashboard/src/components/Card.js index 2c42f3f4b..222e4b4e2 100644 --- a/dashboard/src/components/Card.js +++ b/dashboard/src/components/Card.js @@ -1,10 +1,12 @@ 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 }) => { return ( <> -
+
{!!title && (

diff --git a/dashboard/src/scenes/stats/Blocks.js b/dashboard/src/scenes/stats/Blocks.js index 663e0f50a..b5c3f5021 100644 --- a/dashboard/src/scenes/stats/Blocks.js +++ b/dashboard/src/scenes/stats/Blocks.js @@ -24,10 +24,10 @@ export const BlockDateWithTime = ({ data, field, help }) => { const twoDecimals = (number) => Math.round(number * 100) / 100; -export const BlockTotal = ({ title, unit, data, field, help }) => { +export const BlockTotal = ({ title, unit, data, field, help, onClick = () => null }) => { try { if (!data.length) { - return ; + return ; } const dataWithOnlyNumbers = data.filter((item) => Boolean(item[field])).filter((e) => !isNaN(Number(e[field]))); const total = dataWithOnlyNumbers.reduce((total, item) => total + Number(item[field]), 0); @@ -37,6 +37,7 @@ export const BlockTotal = ({ title, unit, data, field, help }) => { title={title} unit={unit} count={twoDecimals(total)} + onClick={onClick} help={help} children={ diff --git a/dashboard/src/scenes/stats/CustomFieldsStats.js b/dashboard/src/scenes/stats/CustomFieldsStats.js index a235fb98c..ae90c251e 100644 --- a/dashboard/src/scenes/stats/CustomFieldsStats.js +++ b/dashboard/src/scenes/stats/CustomFieldsStats.js @@ -5,7 +5,7 @@ import { BlockDateWithTime, BlockTotal } from './Blocks'; import Card from '../../components/Card'; import { getMultichoiceBarData, getPieData } from './utils'; -const CustomFieldsStats = ({ customFields, data, additionalCols = [], dataTestId = '', help, onSliceClick, totalTitleForMultiChoice }) => { +const CustomFieldsStats = ({ customFields, data, help, onSliceClick, additionalCols = [], dataTestId = '', totalTitleForMultiChoice }) => { const team = useRecoilValue(currentTeamState); const customFieldsInStats = customFields @@ -19,7 +19,14 @@ const CustomFieldsStats = ({ customFields, data, additionalCols = [], dataTestId {additionalCols.map((col) => (

{/* TODO: fix alignment. */} -
} dataTestId={dataTestId} help={help?.(col.title.capitalize())} /> +
} + 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..7be4849d6 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,155 @@ 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)'); + + const allTerritoriesFilters = []; + for (const selectedTerritory of allFilters.selectedTerritories) { + allTerritoriesFilters.push({ + 'Territoire - Nom': selectedTerritory.name, + 'Territoire - _id': selectedTerritory._id, + }); + } + utils.book_append_sheet(wb, utils.json_to_sheet(allTerritoriesFilters), 'Filtres (territoires)'); + const otherFilters = [ + { + 'Voir toute l organisation': allFilters.viewAllOrganisationData, + 'Période - début': allFilters.period.startDate, + 'Période - fin': allFilters.period.endDate, + Preset: allFilters.preset, + }, + ]; + utils.book_append_sheet(wb, utils.json_to_sheet(otherFilters), 'Filtres (autres)'); + const allManuallySelectedTeams = []; + for (const selectedCustomField of allFilters.manuallySelectedTeams) { + allManuallySelectedTeams.push({ + 'Équipe - Nom': selectedCustomField.name, + 'Équipe - _id': selectedCustomField._id, + }); + } + utils.book_append_sheet(wb, utils.json_to_sheet(allManuallySelectedTeams), 'Filtres (équipes)'); + writeFile(wb, `${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 a43b38a3d..3569884b2 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={{ + selectedTerritories, + viewAllOrganisationData, + period, + preset, + manuallySelectedTeams, + }} /> )} {activeTab === 'Comptes-rendus' && }